Skip to content

Commit 8418b18

Browse files
committed
Use early static registration of EventPublishingContextWrapper
Add `OpenTelemetryEventPublisherApplicationListener` which uses a single static `ContextStorage` wrapper which gets applied as early as possible. The static wrapper is then updated as beans come and go. By adding the wrapper early, we hope to avoid silent failures which can occur if the `ContextStorage` gets initialized before the wrapper is added. Closes gh-41439
1 parent 940f826 commit 8418b18

File tree

5 files changed

+271
-16
lines changed

5 files changed

+271
-16
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.micrometer.tracing.exporter.SpanReporter;
2525
import io.micrometer.tracing.otel.bridge.CompositeSpanExporter;
2626
import io.micrometer.tracing.otel.bridge.EventListener;
27-
import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper;
2827
import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
2928
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
3029
import io.micrometer.tracing.otel.bridge.OtelPropagator;
@@ -35,7 +34,6 @@
3534
import io.opentelemetry.api.OpenTelemetry;
3635
import io.opentelemetry.api.metrics.MeterProvider;
3736
import io.opentelemetry.api.trace.Tracer;
38-
import io.opentelemetry.context.ContextStorage;
3937
import io.opentelemetry.context.propagation.ContextPropagators;
4038
import io.opentelemetry.context.propagation.TextMapPropagator;
4139
import io.opentelemetry.sdk.resources.Resource;
@@ -164,8 +162,7 @@ EventPublisher otelTracerEventPublisher(List<EventListener> eventListeners) {
164162

165163
@Bean
166164
@ConditionalOnMissingBean
167-
OtelCurrentTraceContext otelCurrentTraceContext(EventPublisher publisher) {
168-
ContextStorage.addWrapper(new EventPublishingContextWrapper(publisher));
165+
OtelCurrentTraceContext otelCurrentTraceContext() {
169166
return new OtelCurrentTraceContext();
170167
}
171168

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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.util.List;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.function.UnaryOperator;
22+
23+
import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper;
24+
import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
25+
import io.opentelemetry.context.Context;
26+
import io.opentelemetry.context.ContextStorage;
27+
import io.opentelemetry.context.Scope;
28+
29+
import org.springframework.boot.context.event.ApplicationStartingEvent;
30+
import org.springframework.context.ApplicationContext;
31+
import org.springframework.context.ApplicationEvent;
32+
import org.springframework.context.ApplicationListener;
33+
import org.springframework.context.event.ContextClosedEvent;
34+
import org.springframework.context.event.ContextRefreshedEvent;
35+
import org.springframework.context.event.GenericApplicationListener;
36+
import org.springframework.core.Ordered;
37+
import org.springframework.core.ResolvableType;
38+
import org.springframework.util.ClassUtils;
39+
import org.springframework.util.LinkedMultiValueMap;
40+
import org.springframework.util.MultiValueMap;
41+
42+
/**
43+
* {@link ApplicationListener} to add an OpenTelemetry {@link ContextStorage} wrapper for
44+
* {@link EventPublisher} bean support. A single {@link ContextStorage} wrapper is added
45+
* as early as possible then updated with {@link EventPublisher} beans as needed.
46+
*
47+
* @author Phillip Webb
48+
*/
49+
class OpenTelemetryEventPublisherApplicationListener implements GenericApplicationListener {
50+
51+
private static final boolean OTEL_CONTEXT_PRESENT = ClassUtils.isPresent("io.opentelemetry.context.ContextStorage",
52+
null);
53+
54+
private static final boolean MICROMETER_OTEL_PRESENT = ClassUtils
55+
.isPresent("io.micrometer.tracing.otel.bridge.OtelTracer", null);
56+
57+
@Override
58+
public int getOrder() {
59+
return Ordered.HIGHEST_PRECEDENCE;
60+
}
61+
62+
@Override
63+
public boolean supportsEventType(ResolvableType eventType) {
64+
Class<?> type = eventType.getRawClass();
65+
return (type != null) && (ApplicationStartingEvent.class.isAssignableFrom(type)
66+
|| ContextRefreshedEvent.class.isAssignableFrom(type)
67+
|| ContextClosedEvent.class.isAssignableFrom(type));
68+
}
69+
70+
@Override
71+
public void onApplicationEvent(ApplicationEvent event) {
72+
if (!OTEL_CONTEXT_PRESENT || !MICROMETER_OTEL_PRESENT) {
73+
return;
74+
}
75+
if (event instanceof ApplicationStartingEvent) {
76+
EventPublisherBeansContextWrapper.addWrapperIfNecessary();
77+
}
78+
if (event instanceof ContextRefreshedEvent contextRefreshedEvent) {
79+
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
80+
List<EventPublishingContextWrapper> publishers = applicationContext
81+
.getBeansOfType(EventPublisher.class, true, false)
82+
.values()
83+
.stream()
84+
.map(EventPublishingContextWrapper::new)
85+
.toList();
86+
EventPublisherBeansContextWrapper.instance.put(applicationContext, publishers);
87+
}
88+
if (event instanceof ContextClosedEvent contextClosedEvent) {
89+
EventPublisherBeansContextWrapper.instance.remove(contextClosedEvent.getApplicationContext());
90+
}
91+
}
92+
93+
/**
94+
* The single {@link ContextStorage} wrapper that delegates to {@link EventPublisher}
95+
* beans.
96+
*/
97+
static class EventPublisherBeansContextWrapper implements UnaryOperator<ContextStorage> {
98+
99+
private static final AtomicBoolean added = new AtomicBoolean();
100+
101+
private static final EventPublisherBeansContextWrapper instance = new EventPublisherBeansContextWrapper();
102+
103+
private final MultiValueMap<ApplicationContext, EventPublishingContextWrapper> publishers = new LinkedMultiValueMap<>();
104+
105+
private volatile ContextStorage delegate;
106+
107+
static void addWrapperIfNecessary() {
108+
if (added.compareAndSet(false, true)) {
109+
ContextStorage.addWrapper(instance);
110+
}
111+
}
112+
113+
@Override
114+
public ContextStorage apply(ContextStorage contextStorage) {
115+
return new EventPublisherBeansContextStorage(contextStorage);
116+
}
117+
118+
void put(ApplicationContext applicationContext, List<EventPublishingContextWrapper> publishers) {
119+
synchronized (this) {
120+
this.publishers.addAll(applicationContext, publishers);
121+
this.delegate = null;
122+
}
123+
}
124+
125+
void remove(ApplicationContext applicationContext) {
126+
synchronized (this) {
127+
this.publishers.remove(applicationContext);
128+
this.delegate = null;
129+
}
130+
}
131+
132+
private ContextStorage getDelegate(ContextStorage parent) {
133+
ContextStorage delegate = this.delegate;
134+
if (delegate == null) {
135+
synchronized (this) {
136+
delegate = parent;
137+
for (List<EventPublishingContextWrapper> publishers : this.publishers.values()) {
138+
for (EventPublishingContextWrapper publisher : publishers) {
139+
delegate = publisher.apply(delegate);
140+
}
141+
}
142+
}
143+
}
144+
return delegate;
145+
}
146+
147+
/**
148+
* The wrapped {@link ContextStorage} that delegates to the
149+
* {@link EventPublisherBeansContextWrapper}.
150+
*/
151+
class EventPublisherBeansContextStorage implements ContextStorage {
152+
153+
private final ContextStorage parent;
154+
155+
EventPublisherBeansContextStorage(ContextStorage wrapped) {
156+
this.parent = wrapped;
157+
}
158+
159+
@Override
160+
public Scope attach(Context toAttach) {
161+
return getDelegate(this.parent).attach(toAttach);
162+
}
163+
164+
@Override
165+
public Context current() {
166+
return getDelegate(this.parent).current();
167+
}
168+
169+
@Override
170+
public Context root() {
171+
return getDelegate(this.parent).root();
172+
}
173+
174+
}
175+
176+
}
177+
178+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer
1010
# Environment Post Processors
1111
org.springframework.boot.env.EnvironmentPostProcessor=\
1212
org.springframework.boot.actuate.autoconfigure.tracing.LogCorrelationEnvironmentPostProcessor
13+
14+
# Application Listeners
15+
org.springframework.context.ApplicationListener=\
16+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryEventPublisherApplicationListener

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@
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;
3435
import org.springframework.boot.autoconfigure.AutoConfigurations;
3536
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3637
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
3738
import org.springframework.context.ApplicationContext;
39+
import org.springframework.context.ApplicationContextInitializer;
40+
import org.springframework.context.ConfigurableApplicationContext;
3841

3942
import static org.assertj.core.api.Assertions.assertThat;
4043

@@ -55,6 +58,7 @@ class BaggagePropagationIntegrationTests {
5558
@BeforeEach
5659
@AfterEach
5760
void setup() {
61+
EventPublisherBeansContextWrapper.addWrapperIfNecessary();
5862
MDC.clear();
5963
}
6064

@@ -155,27 +159,32 @@ private void assertMdcValue(String key, String expected) {
155159
enum AutoConfig implements Supplier<ApplicationContextRunner> {
156160

157161
BRAVE_DEFAULT {
162+
158163
@Override
159164
public ApplicationContextRunner get() {
160165
return new ApplicationContextRunner()
161166
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
162167
.withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
163168
"management.tracing.baggage.correlation.fields=country-code,bp");
164169
}
170+
165171
},
166172

167173
OTEL_DEFAULT {
174+
168175
@Override
169176
public ApplicationContextRunner get() {
170-
return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
171-
OpenTelemetryAutoConfiguration.class,
172-
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
177+
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
178+
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
179+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
173180
.withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
174181
"management.tracing.baggage.correlation.fields=country-code,bp");
175182
}
183+
176184
},
177185

178186
BRAVE_W3C {
187+
179188
@Override
180189
public ApplicationContextRunner get() {
181190
return new ApplicationContextRunner()
@@ -184,21 +193,25 @@ public ApplicationContextRunner get() {
184193
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
185194
"management.tracing.baggage.correlation.fields=country-code,bp");
186195
}
196+
187197
},
188198

189199
OTEL_W3C {
200+
190201
@Override
191202
public ApplicationContextRunner get() {
192-
return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
193-
OpenTelemetryAutoConfiguration.class,
194-
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
203+
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
204+
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
205+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
195206
.withPropertyValues("management.tracing.propagation.type=W3C",
196207
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
197208
"management.tracing.baggage.correlation.fields=country-code,bp");
198209
}
210+
199211
},
200212

201213
BRAVE_B3 {
214+
202215
@Override
203216
public ApplicationContextRunner get() {
204217
return new ApplicationContextRunner()
@@ -207,9 +220,11 @@ public ApplicationContextRunner get() {
207220
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
208221
"management.tracing.baggage.correlation.fields=country-code,bp");
209222
}
223+
210224
},
211225

212226
BRAVE_B3_MULTI {
227+
213228
@Override
214229
public ApplicationContextRunner get() {
215230
return new ApplicationContextRunner()
@@ -218,40 +233,47 @@ public ApplicationContextRunner get() {
218233
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
219234
"management.tracing.baggage.correlation.fields=country-code,bp");
220235
}
236+
221237
},
222238

223239
OTEL_B3 {
240+
224241
@Override
225242
public ApplicationContextRunner get() {
226-
return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
227-
OpenTelemetryAutoConfiguration.class,
228-
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
243+
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
244+
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
245+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
229246
.withPropertyValues("management.tracing.propagation.type=B3",
230247
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
231248
"management.tracing.baggage.correlation.fields=country-code,bp");
232249
}
250+
233251
},
234252

235253
OTEL_B3_MULTI {
254+
236255
@Override
237256
public ApplicationContextRunner get() {
238-
return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
239-
OpenTelemetryAutoConfiguration.class,
240-
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
257+
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
258+
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
259+
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class))
241260
.withPropertyValues("management.tracing.propagation.type=B3_MULTI",
242261
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
243262
"management.tracing.baggage.correlation.fields=country-code,bp");
244263
}
264+
245265
},
246266

247267
BRAVE_LOCAL_FIELDS {
268+
248269
@Override
249270
public ApplicationContextRunner get() {
250271
return new ApplicationContextRunner()
251272
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
252273
.withPropertyValues("management.tracing.baggage.local-fields=country-code,bp",
253274
"management.tracing.baggage.correlation.fields=country-code,bp");
254275
}
276+
255277
};
256278

257279
boolean isOtel() {
@@ -264,4 +286,14 @@ boolean isBrave() {
264286

265287
}
266288

289+
static class OtelApplicationContextInitializer
290+
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
291+
292+
@Override
293+
public void initialize(ConfigurableApplicationContext applicationContext) {
294+
applicationContext.addApplicationListener(new OpenTelemetryEventPublisherApplicationListener());
295+
}
296+
297+
}
298+
267299
}

0 commit comments

Comments
 (0)