diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index 3bb86387d393..2c2c749b88cb 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -12,6 +12,8 @@ by default, exactly in the following order: xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`]. * `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test instance. +* `MicrometerObservationRegistryTestExecutionListener`: Provides support for + Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`after`" modes. * `TransactionalTestExecutionListener`: Provides transactional test execution with diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index fc4ea1187a5d..a90c378d6d92 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -68,6 +68,8 @@ * ApplicationEventsTestExecutionListener} *
This listener updates the {@link ObservationThreadLocalAccessor} with the + * {@code ObservationRegistry} obtained from the test's {@link ApplicationContext}, + * if present. + * + * @author Marcin Grzejszczak + * @author Sam Brannen + * @since 6.0.10 + */ +class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExecutionListener { + + private static final Log logger = LogFactory.getLog(MicrometerObservationRegistryTestExecutionListener.class); + + private static final String OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME = + "io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor"; + + /** + * Attribute name for a {@link TestContext} attribute which contains the + * {@link ObservationRegistry} that was previously stored in the + * {@link ObservationThreadLocalAccessor}. + *
After each test method, the previously stored {@code ObservationRegistry} + * will be restored. If tests run concurrently this might cause issues unless + * the {@code ObservationRegistry} is always the same (which should typically + * be the case). + */ + private static final String PREVIOUS_OBSERVATION_REGISTRY = Conventions.getQualifiedAttributeName( + MicrometerObservationRegistryTestExecutionListener.class, "previousObservationRegistry"); + + + static { + // Trigger eager resolution of Micrometer Observation types during static + // initialization of this class to ensure that this listener can be properly + // skipped when SpringFactoriesLoader attempts to load it, if micrometer-observation + // is not in the classpath or if the version of ObservationThreadLocalAccessor + // present does not include the getObservationRegistry() method. + String errorMessage = + "MicrometerObservationRegistryTestExecutionListener requires micrometer-observation 1.10.8 or higher"; + Class> clazz; + try { + clazz = Class.forName(OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME, true, + TestExecutionListener.class.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalStateException(errorMessage, ex); + } + + Method method = ReflectionUtils.findMethod(clazz, "getObservationRegistry"); + Assert.state(method != null, errorMessage); + } + + + /** + * Returns {@code 2500}. + */ + @Override + public final int getOrder() { + return 2500; + } + + /** + * If the test's {@link ApplicationContext} contains an {@link ObservationRegistry} + * bean, this method retrieves the {@code ObservationRegistry} currently stored + * in {@link ObservationThreadLocalAccessor}, saves a reference to the original + * registry as a {@link TestContext} attribute (to be restored in + * {@link #afterTestMethod(TestContext)}), and sets the registry from the test's + * {@code ApplicationContext} in {@link ObservationThreadLocalAccessor}. + * @param testContext the test context for the test; never {@code null} + * @see #afterTestMethod(TestContext) + */ + @Override + public void beforeTestMethod(TestContext testContext) { + testContext.getApplicationContext().getBeanProvider(ObservationRegistry.class) + .ifAvailable(registry -> { + if (logger.isDebugEnabled()) { + logger.debug(""" + Registering ObservationRegistry from ApplicationContext in \ + ObservationThreadLocalAccessor for test class \ + """ + testContext.getTestClass().getName()); + } + ObservationThreadLocalAccessor accessor = ObservationThreadLocalAccessor.getInstance(); + testContext.setAttribute(PREVIOUS_OBSERVATION_REGISTRY, accessor.getObservationRegistry()); + accessor.setObservationRegistry(registry); + }); + } + + /** + * Retrieves the original {@link ObservationRegistry} that was saved in + * {@link #beforeTestMethod(TestContext)} and sets it in + * {@link ObservationThreadLocalAccessor}. + * @param testContext the test context for the test; never {@code null} + * @see #beforeTestMethod(TestContext) + */ + @Override + public void afterTestMethod(TestContext testContext) { + ObservationRegistry previousObservationRegistry = + (ObservationRegistry) testContext.removeAttribute(PREVIOUS_OBSERVATION_REGISTRY); + if (previousObservationRegistry != null) { + if (logger.isDebugEnabled()) { + logger.debug("Restoring ObservationRegistry in ObservationThreadLocalAccessor for test class " + + testContext.getTestClass().getName()); + } + ObservationThreadLocalAccessor.getInstance().setObservationRegistry(previousObservationRegistry); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationThreadLocalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationThreadLocalTestExecutionListener.java deleted file mode 100644 index 63aa3b6b3176..000000000000 --- a/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationThreadLocalTestExecutionListener.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2002-2023 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.test.context.observation; - -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; - -import org.springframework.context.ApplicationContext; -import org.springframework.core.Conventions; -import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; -import org.springframework.test.context.support.AbstractTestExecutionListener; - -/** - * {@code ObservationThreadLocalTestExecutionListener} is an implementation of the {@link TestExecutionListener} - * SPI that updates the {@link ObservationThreadLocalAccessor} with the {@link ObservationRegistry} - * taken from the {@link ApplicationContext} present in the {@link TestContext}. - * - *
This implementation is not thread-safe. - * - * @author Marcin Grzejszczak - * @since 6.1 - */ -public class MicrometerObservationThreadLocalTestExecutionListener extends AbstractTestExecutionListener { - - /** - * Attribute name for a {@link TestContext} attribute which contains the previously - * set {@link ObservationRegistry} on the {@link ObservationThreadLocalAccessor}. - *
After all tests from the current test class have completed, the previously stored {@link ObservationRegistry}
- * will be restored. If tests are ran concurrently this might cause issues
- * unless the {@link ObservationRegistry} is always the same (which should be the case most frequently).
- */
- private static final String PREVIOUS_OBSERVATION_REGISTRY = Conventions.getQualifiedAttributeName(
- MicrometerObservationThreadLocalTestExecutionListener.class, "previousObservationRegistry");
-
- /**
- * Retrieves the current {@link ObservationRegistry} stored
- * on {@link ObservationThreadLocalAccessor} instance and stores it
- * in the {@link TestContext} attributes and overrides it with
- * one stored in {@link ApplicationContext} associated with
- * the {@link TestContext}.
- * @param testContext the test context for the test; never {@code null}
- */
- @Override
- public void beforeTestMethod(TestContext testContext) {
- testContext.setAttribute(PREVIOUS_OBSERVATION_REGISTRY,
- ObservationThreadLocalAccessor.getInstance().getObservationRegistry());
- testContext.getApplicationContext()
- .getBeanProvider(ObservationRegistry.class)
- .ifAvailable(observationRegistry ->
- ObservationThreadLocalAccessor.getInstance()
- .setObservationRegistry(observationRegistry));
- }
-
- /**
- * Retrieves the previously stored {@link ObservationRegistry} and sets it back
- * on the {@link ObservationThreadLocalAccessor} instance.
- * @param testContext the test context for the test; never {@code null}
- */
- @Override
- public void afterTestMethod(TestContext testContext) {
- ObservationRegistry previousObservationRegistry =
- (ObservationRegistry) testContext.getAttribute(PREVIOUS_OBSERVATION_REGISTRY);
- if (previousObservationRegistry != null) {
- ObservationThreadLocalAccessor.getInstance()
- .setObservationRegistry(previousObservationRegistry);
- }
- }
-
-
- /**
- * Returns {@code 3500}.
- */
- @Override
- public final int getOrder() {
- return 3500;
- }
-}
diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories
index 4bb2ddbef728..2b9e4e3c116f 100644
--- a/spring-test/src/main/resources/META-INF/spring.factories
+++ b/spring-test/src/main/resources/META-INF/spring.factories
@@ -5,11 +5,11 @@ org.springframework.test.context.TestExecutionListener = \
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
+ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
- org.springframework.test.context.event.EventPublishingTestExecutionListener,\
- org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener
+ org.springframework.test.context.event.EventPublishingTestExecutionListener
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
#
diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
index e83025b2281c..a1e634c31807 100644
--- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -28,13 +28,13 @@
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
-import org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.ServletTestExecutionListener;
+import org.springframework.util.ClassUtils;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
@@ -57,14 +57,17 @@
*/
class TestExecutionListenersTests {
+ private static final Class> micrometerListenerClass =
+ ClassUtils.resolveClassName("org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener", null);
+
@Test
void defaultListeners() {
List