Skip to content

Commit 81853d4

Browse files
committed
Use early static registration of EventPublishingContextWrapper in tests
Add `OpenTelemetryEventPublisherBeansTestExecutionListener` JUnit class to automatically trigger early addition of the `ContextStorage` wrapper. The listener has also been updated with a static `addWrapper()` method that can be called directly for other test frameworks. Closes gh-42005
1 parent 9659be2 commit 81853d4

File tree

8 files changed

+152
-48
lines changed

8 files changed

+152
-48
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ dependencies {
106106
optional("org.hibernate.orm:hibernate-micrometer")
107107
optional("org.hibernate.validator:hibernate-validator")
108108
optional("org.influxdb:influxdb-java")
109+
optional("org.junit.platform:junit-platform-launcher")
109110
optional("org.liquibase:liquibase-core") {
110111
exclude group: "javax.xml.bind", module: "jaxb-api"
111112
}
Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.List;
2020
import java.util.concurrent.atomic.AtomicBoolean;
21-
import java.util.function.UnaryOperator;
2221

2322
import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper;
2423
import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
@@ -42,18 +41,26 @@
4241
/**
4342
* {@link ApplicationListener} to add an OpenTelemetry {@link ContextStorage} wrapper for
4443
* {@link EventPublisher} bean support. A single {@link ContextStorage} wrapper is added
45-
* as early as possible then updated with {@link EventPublisher} beans as needed.
44+
* on the {@link ApplicationStartingEvent} then updated with {@link EventPublisher} beans
45+
* as needed.
46+
* <p>
47+
* The {@link #addWrapper()} method may also be called directly if the
48+
* {@link ApplicationStartingEvent} isn't called early enough or isn't fired.
4649
*
4750
* @author Phillip Webb
51+
* @since 3.4.0
52+
* @see OpenTelemetryEventPublisherBeansTestExecutionListener
4853
*/
49-
class OpenTelemetryEventPublisherApplicationListener implements GenericApplicationListener {
54+
public class OpenTelemetryEventPublisherBeansApplicationListener implements GenericApplicationListener {
5055

5156
private static final boolean OTEL_CONTEXT_PRESENT = ClassUtils.isPresent("io.opentelemetry.context.ContextStorage",
5257
null);
5358

5459
private static final boolean MICROMETER_OTEL_PRESENT = ClassUtils
5560
.isPresent("io.micrometer.tracing.otel.bridge.OtelTracer", null);
5661

62+
private static final AtomicBoolean added = new AtomicBoolean();
63+
5764
@Override
5865
public int getOrder() {
5966
return Ordered.HIGHEST_PRECEDENCE;
@@ -69,11 +76,11 @@ public boolean supportsEventType(ResolvableType eventType) {
6976

7077
@Override
7178
public void onApplicationEvent(ApplicationEvent event) {
72-
if (!OTEL_CONTEXT_PRESENT || !MICROMETER_OTEL_PRESENT) {
79+
if (!isInstallable()) {
7380
return;
7481
}
7582
if (event instanceof ApplicationStartingEvent) {
76-
EventPublisherBeansContextWrapper.addWrapperIfNecessary();
83+
addWrapper();
7784
}
7885
if (event instanceof ContextRefreshedEvent contextRefreshedEvent) {
7986
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
@@ -83,92 +90,105 @@ public void onApplicationEvent(ApplicationEvent event) {
8390
.stream()
8491
.map(EventPublishingContextWrapper::new)
8592
.toList();
86-
EventPublisherBeansContextWrapper.instance.put(applicationContext, publishers);
93+
Wrapper.instance.put(applicationContext, publishers);
8794
}
8895
if (event instanceof ContextClosedEvent contextClosedEvent) {
89-
EventPublisherBeansContextWrapper.instance.remove(contextClosedEvent.getApplicationContext());
96+
Wrapper.instance.remove(contextClosedEvent.getApplicationContext());
9097
}
9198
}
9299

93100
/**
94-
* The single {@link ContextStorage} wrapper that delegates to {@link EventPublisher}
95-
* beans.
101+
* {@link ContextStorage#addWrapper(java.util.function.Function) Add} the
102+
* {@link ContextStorage} wrapper to ensure that {@link EventPublisher} are propagated
103+
* correctly.
96104
*/
97-
static class EventPublisherBeansContextWrapper implements UnaryOperator<ContextStorage> {
105+
public static void addWrapper() {
106+
if (isInstallable() && added.compareAndSet(false, true)) {
107+
Wrapper.instance.addWrapper();
108+
}
109+
}
110+
111+
private static boolean isInstallable() {
112+
return OTEL_CONTEXT_PRESENT && MICROMETER_OTEL_PRESENT;
113+
}
98114

99-
private static final AtomicBoolean added = new AtomicBoolean();
115+
/**
116+
* Single instance class used to add the wrapper and manage the {@link EventPublisher}
117+
* beans.
118+
*/
119+
static final class Wrapper {
100120

101-
private static final EventPublisherBeansContextWrapper instance = new EventPublisherBeansContextWrapper();
121+
static Wrapper instance = new Wrapper();
102122

103-
private final MultiValueMap<ApplicationContext, EventPublishingContextWrapper> publishers = new LinkedMultiValueMap<>();
123+
private final MultiValueMap<ApplicationContext, EventPublishingContextWrapper> beans = new LinkedMultiValueMap<>();
104124

105-
private volatile ContextStorage delegate;
125+
private volatile ContextStorage storageDelegate;
106126

107-
static void addWrapperIfNecessary() {
108-
if (added.compareAndSet(false, true)) {
109-
ContextStorage.addWrapper(instance);
110-
}
127+
private Wrapper() {
111128
}
112129

113-
@Override
114-
public ContextStorage apply(ContextStorage contextStorage) {
115-
return new EventPublisherBeansContextStorage(contextStorage);
130+
private void addWrapper() {
131+
ContextStorage.addWrapper(Storage::new);
116132
}
117133

118134
void put(ApplicationContext applicationContext, List<EventPublishingContextWrapper> publishers) {
119135
synchronized (this) {
120-
this.publishers.addAll(applicationContext, publishers);
121-
this.delegate = null;
136+
this.beans.addAll(applicationContext, publishers);
137+
this.storageDelegate = null;
122138
}
123139
}
124140

125141
void remove(ApplicationContext applicationContext) {
126142
synchronized (this) {
127-
this.publishers.remove(applicationContext);
128-
this.delegate = null;
143+
this.beans.remove(applicationContext);
144+
this.storageDelegate = null;
129145
}
130146
}
131147

132-
private ContextStorage getDelegate(ContextStorage parent) {
133-
ContextStorage delegate = this.delegate;
148+
ContextStorage getStorageDelegate(ContextStorage parent) {
149+
ContextStorage delegate = this.storageDelegate;
134150
if (delegate == null) {
135151
synchronized (this) {
136152
delegate = parent;
137-
for (List<EventPublishingContextWrapper> publishers : this.publishers.values()) {
153+
for (List<EventPublishingContextWrapper> publishers : this.beans.values()) {
138154
for (EventPublishingContextWrapper publisher : publishers) {
139155
delegate = publisher.apply(delegate);
140156
}
141157
}
158+
this.storageDelegate = delegate;
142159
}
143160
}
144161
return delegate;
145162
}
146163

147164
/**
148-
* The wrapped {@link ContextStorage} that delegates to the
149-
* {@link EventPublisherBeansContextWrapper}.
165+
* {@link ContextStorage} that delegates to the {@link EventPublisher} beans.
150166
*/
151-
class EventPublisherBeansContextStorage implements ContextStorage {
167+
class Storage implements ContextStorage {
152168

153169
private final ContextStorage parent;
154170

155-
EventPublisherBeansContextStorage(ContextStorage wrapped) {
156-
this.parent = wrapped;
171+
Storage(ContextStorage parent) {
172+
this.parent = parent;
157173
}
158174

159175
@Override
160176
public Scope attach(Context toAttach) {
161-
return getDelegate(this.parent).attach(toAttach);
177+
return getDelegate().attach(toAttach);
162178
}
163179

164180
@Override
165181
public Context current() {
166-
return getDelegate(this.parent).current();
182+
return getDelegate().current();
167183
}
168184

169185
@Override
170186
public Context root() {
171-
return getDelegate(this.parent).root();
187+
return getDelegate().root();
188+
}
189+
190+
private ContextStorage getDelegate() {
191+
return getStorageDelegate(this.parent);
172192
}
173193

174194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.actuate.autoconfigure.tracing;
18+
19+
import org.junit.platform.launcher.TestExecutionListener;
20+
import org.junit.platform.launcher.TestIdentifier;
21+
22+
/**
23+
* JUnit {@link TestExecutionListener} to ensure
24+
* {@link OpenTelemetryEventPublisherBeansApplicationListener#addWrapper()} is called as
25+
* early as possible.
26+
*
27+
* @author Phillip Webb
28+
* @since 3.4.0
29+
* @see OpenTelemetryEventPublisherBeansApplicationListener
30+
*/
31+
public class OpenTelemetryEventPublisherBeansTestExecutionListener implements TestExecutionListener {
32+
33+
@Override
34+
public void executionStarted(TestIdentifier testIdentifier) {
35+
OpenTelemetryEventPublisherBeansApplicationListener.addWrapper();
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansTestExecutionListener

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ org.springframework.boot.actuate.autoconfigure.tracing.LogCorrelationEnvironment
1313

1414
# Application Listeners
1515
org.springframework.context.ApplicationListener=\
16-
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener
16+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansApplicationListener

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.slf4j.MDC;
3232

3333
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
34-
import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener.EventPublisherBeansContextWrapper;
3534
import org.springframework.boot.autoconfigure.AutoConfigurations;
3635
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3736
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
@@ -58,7 +57,7 @@ class BaggagePropagationIntegrationTests {
5857
@BeforeEach
5958
@AfterEach
6059
void setup() {
61-
EventPublisherBeansContextWrapper.addWrapperIfNecessary();
60+
OpenTelemetryEventPublisherBeansApplicationListener.addWrapper();
6261
MDC.clear();
6362
}
6463

@@ -291,7 +290,7 @@ static class OtelApplicationContextInitializer
291290

292291
@Override
293292
public void initialize(ConfigurableApplicationContext applicationContext) {
294-
applicationContext.addApplicationListener(new OpenTelemetryEventPublisherApplicationListener());
293+
applicationContext.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener());
295294
}
296295

297296
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.actuate.autoconfigure.tracing;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.List;
21+
import java.util.function.Function;
22+
23+
import io.opentelemetry.context.ContextStorage;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherBeansApplicationListener.Wrapper.Storage;
27+
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.mockito.Mockito.mock;
31+
32+
/**
33+
* Integration tests for {@link OpenTelemetryEventPublisherBeansTestExecutionListener}.
34+
*
35+
* @author Phillip Webb
36+
*/
37+
@ForkedClassPath
38+
class OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests {
39+
40+
private final ContextStorage parent = mock(ContextStorage.class);
41+
42+
@Test
43+
@SuppressWarnings({ "unchecked", "rawtypes" })
44+
void wrapperIsInstalled() throws Exception {
45+
Class<?> wrappersClass = Class.forName("io.opentelemetry.context.ContextStorageWrappers");
46+
Method getWrappersMethod = wrappersClass.getDeclaredMethod("getWrappers");
47+
getWrappersMethod.setAccessible(true);
48+
List<Function> wrappers = (List<Function>) getWrappersMethod.invoke(null);
49+
assertThat(wrappers).anyMatch((function) -> function.apply(this.parent) instanceof Storage);
50+
}
51+
52+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryTracingAutoConfigurationTests.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,12 @@
5454
import io.opentelemetry.sdk.trace.export.SpanExporter;
5555
import io.opentelemetry.sdk.trace.samplers.Sampler;
5656
import org.assertj.core.api.InstanceOfAssertFactories;
57-
import org.junit.jupiter.api.BeforeAll;
5857
import org.junit.jupiter.api.Test;
5958
import org.junit.jupiter.params.ParameterizedTest;
6059
import org.junit.jupiter.params.provider.ValueSource;
6160
import org.mockito.Mockito;
6261

6362
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
64-
import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener.EventPublisherBeansContextWrapper;
6563
import org.springframework.boot.autoconfigure.AutoConfigurations;
6664
import org.springframework.boot.context.annotation.Configurations;
6765
import org.springframework.boot.test.context.FilteredClassLoader;
@@ -93,11 +91,6 @@ class OpenTelemetryTracingAutoConfigurationTests {
9391
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class,
9492
OpenTelemetryTracingAutoConfiguration.class));
9593

96-
@BeforeAll
97-
static void addWrapper() {
98-
EventPublisherBeansContextWrapper.addWrapperIfNecessary();
99-
}
100-
10194
@Test
10295
void shouldSupplyBeans() {
10396
this.contextRunner.run((context) -> {
@@ -354,7 +347,7 @@ void shouldUseReplacementForDeprecatedVersion() {
354347
}
355348

356349
private void initializeOpenTelemetry(ConfigurableApplicationContext context) {
357-
context.addApplicationListener(new OpenTelemetryEventPublisherApplicationListener());
350+
context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener());
358351
Span.current();
359352
}
360353

0 commit comments

Comments
 (0)