Overhaul TestExecutionListener for Micrometer ObservationRegistry
This commit overhauls the TestExecutionListener for Micrometer's ObservationRegistry that was introduced in the previous commit. Specifically, this commit: - Renames the listener to MicrometerObservationRegistryTestExecutionListener since the use of a ThreadLocal is an implementation detail that may change over time. - Makes the listener package-private instead of public in order to allow the team greater flexibility in evolving this feature. - Eagerly loads the ObservationThreadLocalAccessor class and verifies that it has a getObservationRegistry() method to ensure that the listener is properly skipped when SpringFactoriesLoader attempts to load it, if Micrometer 1.10.8+ is not on the classpath. - Switches the listener's automatic registration order to 2500 in order to register it after the DependencyInjectionTestExecutionListener. - Only tracks the previous ObservationRegistry in beforeTestMethod() if the test's ApplicationContext contains an ObservationRegistry bean. - Properly removes the TestContext attribute for the previous ObservationRegistry in afterTestMethod(). - Introduces DEBUG logging for diagnostics. - Adds an entry in the Javadoc for TestExecutionListener as well as in the Testing chapter in the reference manual. Closes gh-30658
This commit is contained in:
@@ -68,6 +68,8 @@ package org.springframework.test.context;
|
||||
* ApplicationEventsTestExecutionListener}</li>
|
||||
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener
|
||||
* DependencyInjectionTestExecutionListener}</li>
|
||||
* <li>{@link org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener
|
||||
* MicrometerObservationRegistryTestExecutionListener}</li>
|
||||
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener
|
||||
* DirtiesContextTestExecutionListener}</li>
|
||||
* <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 java.lang.reflect.Method;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.platform.launcher.TestExecutionListener;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.support.AbstractTestExecutionListener;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@code TestExecutionListener} which provides support for Micrometer's
|
||||
* {@link ObservationRegistry}.
|
||||
*
|
||||
* <p>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}.
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>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}.
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user