Skip to content

Commit a82659c

Browse files
Introduce TestExecutionListener for Micrometer ObservationRegistry
Prior to this commit, there was no way to specify the ObservationRegistry that is registered in the given test's ApplicationContext as the one that should be used by Micrometer's ObservationThreadLocalAccessor for context propagation. This commit introduces a TestExecutionListener for Micrometer's ObservationRegistry in the Spring TestContext Framework. Specifically, this listener obtains the ObservationRegistry registered in the test's ApplicationContext, stores it in ObservationThreadLocalAccessor for the duration of each test method execution, and restores the original ObservationRegistry in ObservationThreadLocalAccessor after each test. Co-authored-by: Sam Brannen <[email protected]> See gh-30658
1 parent 3415b04 commit a82659c

File tree

6 files changed

+194
-2
lines changed

6 files changed

+194
-2
lines changed

spring-test/spring-test.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dependencies {
5050
optional("io.projectreactor:reactor-test")
5151
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core")
5252
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
53+
optional('io.micrometer:context-propagation')
54+
optional('io.micrometer:micrometer-observation')
5355
testImplementation(project(":spring-core-test"))
5456
testImplementation(project(":spring-context-support"))
5557
testImplementation(project(":spring-oxm"))
@@ -58,7 +60,6 @@ dependencies {
5860
testImplementation(testFixtures(project(":spring-core")))
5961
testImplementation(testFixtures(project(":spring-tx")))
6062
testImplementation(testFixtures(project(":spring-web")))
61-
testImplementation('io.micrometer:context-propagation')
6263
testImplementation("jakarta.annotation:jakarta.annotation-api")
6364
testImplementation("javax.cache:cache-api")
6465
testImplementation("jakarta.ejb:jakarta.ejb-api")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.observation;
18+
19+
import io.micrometer.observation.ObservationRegistry;
20+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
21+
22+
import org.springframework.context.ApplicationContext;
23+
import org.springframework.core.Conventions;
24+
import org.springframework.test.context.TestContext;
25+
import org.springframework.test.context.TestExecutionListener;
26+
import org.springframework.test.context.support.AbstractTestExecutionListener;
27+
28+
/**
29+
* {@code ObservationThreadLocalTestExecutionListener} is an implementation of the {@link TestExecutionListener}
30+
* SPI that updates the {@link ObservationThreadLocalAccessor} with the {@link ObservationRegistry}
31+
* taken from the {@link ApplicationContext} present in the {@link TestContext}.
32+
*
33+
* <p>This implementation is not thread-safe.
34+
*
35+
* @author Marcin Grzejszczak
36+
* @since 6.1
37+
*/
38+
public class MicrometerObservationThreadLocalTestExecutionListener extends AbstractTestExecutionListener {
39+
40+
/**
41+
* Attribute name for a {@link TestContext} attribute which contains the previously
42+
* set {@link ObservationRegistry} on the {@link ObservationThreadLocalAccessor}.
43+
* <p>After all tests from the current test class have completed, the previously stored {@link ObservationRegistry}
44+
* will be restored. If tests are ran concurrently this might cause issues
45+
* unless the {@link ObservationRegistry} is always the same (which should be the case most frequently).
46+
*/
47+
private static final String PREVIOUS_OBSERVATION_REGISTRY = Conventions.getQualifiedAttributeName(
48+
MicrometerObservationThreadLocalTestExecutionListener.class, "previousObservationRegistry");
49+
50+
/**
51+
* Retrieves the current {@link ObservationRegistry} stored
52+
* on {@link ObservationThreadLocalAccessor} instance and stores it
53+
* in the {@link TestContext} attributes and overrides it with
54+
* one stored in {@link ApplicationContext} associated with
55+
* the {@link TestContext}.
56+
* @param testContext the test context for the test; never {@code null}
57+
*/
58+
@Override
59+
public void beforeTestMethod(TestContext testContext) {
60+
testContext.setAttribute(PREVIOUS_OBSERVATION_REGISTRY,
61+
ObservationThreadLocalAccessor.getInstance().getObservationRegistry());
62+
testContext.getApplicationContext()
63+
.getBeanProvider(ObservationRegistry.class)
64+
.ifAvailable(observationRegistry ->
65+
ObservationThreadLocalAccessor.getInstance()
66+
.setObservationRegistry(observationRegistry));
67+
}
68+
69+
/**
70+
* Retrieves the previously stored {@link ObservationRegistry} and sets it back
71+
* on the {@link ObservationThreadLocalAccessor} instance.
72+
* @param testContext the test context for the test; never {@code null}
73+
*/
74+
@Override
75+
public void afterTestMethod(TestContext testContext) {
76+
ObservationRegistry previousObservationRegistry =
77+
(ObservationRegistry) testContext.getAttribute(PREVIOUS_OBSERVATION_REGISTRY);
78+
if (previousObservationRegistry != null) {
79+
ObservationThreadLocalAccessor.getInstance()
80+
.setObservationRegistry(previousObservationRegistry);
81+
}
82+
}
83+
84+
85+
/**
86+
* Returns {@code 3500}.
87+
*/
88+
@Override
89+
public final int getOrder() {
90+
return 3500;
91+
}
92+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Observation support classes for the <em>Spring TestContext Framework</em>.
3+
*/
4+
@NonNullApi
5+
@NonNullFields
6+
package org.springframework.test.context.observation;
7+
8+
import org.springframework.lang.NonNullApi;
9+
import org.springframework.lang.NonNullFields;

spring-test/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ org.springframework.test.context.TestExecutionListener = \
88
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
99
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
1010
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
11-
org.springframework.test.context.event.EventPublishingTestExecutionListener
11+
org.springframework.test.context.event.EventPublishingTestExecutionListener,\
12+
org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener
1213

1314
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
1415
#

spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
2929
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
3030
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
31+
import org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener;
3132
import org.springframework.test.context.support.AbstractTestExecutionListener;
3233
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
3334
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
@@ -63,6 +64,7 @@ void defaultListeners() {
6364
ApplicationEventsTestExecutionListener.class,//
6465
DependencyInjectionTestExecutionListener.class,//
6566
DirtiesContextTestExecutionListener.class,//
67+
MicrometerObservationThreadLocalTestExecutionListener.class,//
6668
TransactionalTestExecutionListener.class,//
6769
SqlScriptsTestExecutionListener.class,//
6870
EventPublishingTestExecutionListener.class
@@ -81,6 +83,7 @@ void defaultListenersMergedWithCustomListenerPrepended() {
8183
ApplicationEventsTestExecutionListener.class,//
8284
DependencyInjectionTestExecutionListener.class,//
8385
DirtiesContextTestExecutionListener.class,//
86+
MicrometerObservationThreadLocalTestExecutionListener.class,//
8487
TransactionalTestExecutionListener.class,//
8588
SqlScriptsTestExecutionListener.class,//
8689
EventPublishingTestExecutionListener.class
@@ -98,6 +101,7 @@ void defaultListenersMergedWithCustomListenerAppended() {
98101
ApplicationEventsTestExecutionListener.class,//
99102
DependencyInjectionTestExecutionListener.class,//
100103
DirtiesContextTestExecutionListener.class,//
104+
MicrometerObservationThreadLocalTestExecutionListener.class,//
101105
TransactionalTestExecutionListener.class,
102106
SqlScriptsTestExecutionListener.class,//
103107
EventPublishingTestExecutionListener.class,//
@@ -117,6 +121,7 @@ void defaultListenersMergedWithCustomListenerInserted() {
117121
DependencyInjectionTestExecutionListener.class,//
118122
BarTestExecutionListener.class,//
119123
DirtiesContextTestExecutionListener.class,//
124+
MicrometerObservationThreadLocalTestExecutionListener.class,//
120125
TransactionalTestExecutionListener.class,//
121126
SqlScriptsTestExecutionListener.class,//
122127
EventPublishingTestExecutionListener.class
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.observation;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import io.micrometer.observation.ObservationRegistry;
23+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.context.support.StaticApplicationContext;
28+
import org.springframework.test.context.TestContext;
29+
30+
import static org.assertj.core.api.BDDAssertions.then;
31+
import static org.mockito.ArgumentMatchers.any;
32+
import static org.mockito.ArgumentMatchers.anyString;
33+
import static org.mockito.BDDMockito.given;
34+
import static org.mockito.BDDMockito.willAnswer;
35+
import static org.mockito.Mockito.mock;
36+
37+
class MicrometerObservationThreadLocalTestExecutionListenerTests {
38+
39+
ObservationRegistry originalObservationRegistry = ObservationThreadLocalAccessor.getInstance().getObservationRegistry();
40+
41+
TestContext testContext = mock();
42+
43+
StaticApplicationContext applicationContext = new StaticApplicationContext();
44+
45+
Map<String, Object> attributes = new HashMap<>();
46+
47+
MicrometerObservationThreadLocalTestExecutionListener listener = new MicrometerObservationThreadLocalTestExecutionListener();
48+
49+
@BeforeEach
50+
void setup() {
51+
willAnswer(invocation -> attributes.put(invocation.getArgument(0), invocation.getArgument(1))).given(testContext).setAttribute(anyString(), any());
52+
given(testContext.getAttribute(anyString())).willAnswer(invocation -> attributes.get(invocation.getArgument(0, String.class)));
53+
given(testContext.getApplicationContext()).willReturn(applicationContext);
54+
}
55+
56+
@Test
57+
void observationRegistryShouldNotBeOverridden() throws Exception {
58+
listener.beforeTestMethod(testContext);
59+
thenObservationRegistryOnOTLAIsSameAsOriginal();
60+
listener.afterTestMethod(testContext);
61+
thenObservationRegistryOnOTLAIsSameAsOriginal();
62+
}
63+
64+
@Test
65+
void observationRegistryOverriddenByBeanFromTestContext() throws Exception {
66+
ObservationRegistry newObservationRegistry = ObservationRegistry.create();
67+
applicationContext.getDefaultListableBeanFactory().registerSingleton("observationRegistry", newObservationRegistry);
68+
69+
listener.beforeTestMethod(testContext);
70+
ObservationRegistry otlaObservationRegistry = ObservationThreadLocalAccessor.getInstance().getObservationRegistry();
71+
then(otlaObservationRegistry)
72+
.as("During the test we want the original ObservationRegistry to be replaced with the one present in this application context")
73+
.isNotSameAs(originalObservationRegistry)
74+
.isSameAs(newObservationRegistry);
75+
76+
listener.afterTestMethod(testContext);
77+
thenObservationRegistryOnOTLAIsSameAsOriginal();
78+
}
79+
80+
private void thenObservationRegistryOnOTLAIsSameAsOriginal() {
81+
then(ObservationThreadLocalAccessor.getInstance().getObservationRegistry()).isSameAs(originalObservationRegistry);
82+
}
83+
84+
}

0 commit comments

Comments
 (0)