Skip to content

Commit

Permalink
Improve logging in MicrometerObservationRegistryTestExecutionListener
Browse files Browse the repository at this point in the history
Prior to this commit, dependency checks in the static initialization
block for MicrometerObservationRegistryTestExecutionListener resulted
in an ExceptionInInitializerError which led to verbose logging in
TestContextFailureHandler.

This commit improves the logging for missing dependencies in
MicrometerObservationRegistryTestExecutionListener by moving the
dependency checks to the constructor and by throwing a
NoClassDefFoundError instead of an IllegalStateException. This allows
TestContextFailureHandler to log a concise DEBUG message denoting that
the listener is being skipped due to missing dependencies.

This commit also now checks for the presence of
io.micrometer.context.ThreadLocalAccessor in addition to
io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor.

Furthermore, this commit now explicitly mentions the need for
io.micrometer:context-propagation in the error message.

The following demonstrate the generated DEBUB message when
ObservationThreadLocalAccessor and ThreadLocalAccessor are missing,
respectively.

Skipping candidate TestExecutionListener [org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener] due to a missing dependency. Specify custom TestExecutionListener classes or make the default TestExecutionListener classes and their required dependencies available. Offending class: io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor. MicrometerObservationRegistryTestExecutionListener requires io.micrometer:micrometer-observation:1.10.8 or higher and io.micrometer:context-propagation:1.0.3 or higher.

Skipping candidate TestExecutionListener [org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener] due to a missing dependency. Specify custom TestExecutionListener classes or make the default TestExecutionListener classes and their required dependencies available. Offending class: io.micrometer.context.ThreadLocalAccessor. MicrometerObservationRegistryTestExecutionListener requires io.micrometer:micrometer-observation:1.10.8 or higher and io.micrometer:context-propagation:1.0.3 or higher.

Closes spring-projectsgh-30747
  • Loading branch information
sbrannen authored and mdeinum committed Jun 29, 2023
1 parent 19d776a commit a75e6af
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 17 deletions.
Expand Up @@ -26,9 +26,7 @@
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;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
Expand All @@ -47,6 +45,8 @@ class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExe

private static final Log logger = LogFactory.getLog(MicrometerObservationRegistryTestExecutionListener.class);

private static final String THREAD_LOCAL_ACCESSOR_CLASS_NAME = "io.micrometer.context.ThreadLocalAccessor";

private static final String OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME =
"io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor";

Expand All @@ -63,25 +63,41 @@ class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExe
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
public MicrometerObservationRegistryTestExecutionListener() {
// Trigger eager resolution of Micrometer Observation types to ensure that
// this listener can be properly skipped when SpringFactoriesLoader attempts
// to load it -- for example, if context-propagation and micrometer-observation
// are 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;
ClassLoader classLoader = getClass().getClassLoader();
String errorMessage = """
MicrometerObservationRegistryTestExecutionListener requires \
io.micrometer:micrometer-observation:1.10.8 or higher and \
io.micrometer:context-propagation:1.0.3 or higher.""";

try {
clazz = Class.forName(OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME, true,
TestExecutionListener.class.getClassLoader());
Class.forName(THREAD_LOCAL_ACCESSOR_CLASS_NAME, false, classLoader);
Class<?> clazz = Class.forName(OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME, false, classLoader);
Method method = ReflectionUtils.findMethod(clazz, "getObservationRegistry");
if (method == null) {
// We simulate "class not found" even though it's "method not found", since
// the ClassNotFoundException will be processed in the subsequent catch-block.
throw new ClassNotFoundException(OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME);
}
}
catch (Throwable ex) {
throw new IllegalStateException(errorMessage, ex);
// Ensure that we throw a NoClassDefFoundError so that the exception will be properly
// handled in TestContextFailureHandler.
// If the original exception was a ClassNotFoundException or NoClassDefFoundError,
// we throw a NoClassDefFoundError with an augmented error message and omit the cause.
if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
throw new NoClassDefFoundError(ex.getMessage() + ". " + errorMessage);
}
// Otherwise, we throw a NoClassDefFoundError with the cause initialized.
Error error = new NoClassDefFoundError(errorMessage);
error.initCause(ex);
throw error;
}

Method method = ReflectionUtils.findMethod(clazz, "getObservationRegistry");
Assert.state(method != null, errorMessage);
}


Expand Down
Expand Up @@ -43,7 +43,7 @@ public void handleFailure(Class<?> factoryType, String factoryImplementationName
logger.debug("""
Skipping candidate %1$s [%2$s] due to a missing dependency. \
Specify custom %1$s classes or make the default %1$s classes \
and their required dependencies available. Offending class: [%3$s]"""
and their required dependencies available. Offending class: %3$s"""
.formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
}
}
Expand Down

0 comments on commit a75e6af

Please sign in to comment.