Skip to content

Commit d9921f0

Browse files
authored
Fix regression when using Spring @EnableAsync (#15249)
1 parent d8b9ad3 commit d9921f0

File tree

6 files changed

+97
-0
lines changed

6 files changed

+97
-0
lines changed

instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,11 @@ protected void configure(HttpServerTestOptions options) {
5959
Boolean.getBoolean("testLatestDeps") ? 500 : 200);
6060
options.setExpectedException(new RuntimeException(EXCEPTION.getBody()));
6161
}
62+
63+
@Override
64+
protected boolean shouldTestDeferredResult() {
65+
// older versions of Spring Boot don't properly propagate context to async calls,
66+
// resulting in a separate trace instead of a single trace
67+
return Boolean.getBoolean("testLatestDeps");
68+
}
6269
}

instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE;
2323
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE;
2424
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2526

2627
import io.opentelemetry.api.common.AttributeKey;
2728
import io.opentelemetry.api.trace.SpanKind;
@@ -51,13 +52,20 @@
5152
public abstract class AbstractSpringBootBasedTest
5253
extends AbstractHttpServerTest<ConfigurableApplicationContext> {
5354

55+
static final ServerEndpoint DEFERRED_RESULT =
56+
new ServerEndpoint("DEFERRED_RESULT", "deferred-result", 200, "deferred result");
57+
5458
private static final String EXPERIMENTAL_SPAN_CONFIG =
5559
"otel.instrumentation.spring-webmvc.experimental-span-attributes";
5660

5761
protected abstract ConfigurableApplicationContext context();
5862

5963
protected abstract Class<?> securityConfigClass();
6064

65+
protected boolean shouldTestDeferredResult() {
66+
return true;
67+
}
68+
6169
@Override
6270
protected void stopServer(ConfigurableApplicationContext ctx) {
6371
ctx.close();
@@ -144,6 +152,39 @@ void testCharacterEncodingOfTestPassword(String testPassword) {
144152
.hasKind(SpanKind.INTERNAL)));
145153
}
146154

155+
@Test
156+
void deferredResult() {
157+
assumeTrue(shouldTestDeferredResult());
158+
159+
AggregatedHttpResponse response =
160+
client.execute(request(DEFERRED_RESULT, "GET")).aggregate().join();
161+
162+
assertThat(response.status().code()).isEqualTo(DEFERRED_RESULT.getStatus());
163+
assertThat(response.contentUtf8()).isEqualTo(DEFERRED_RESULT.getBody());
164+
165+
testing()
166+
.waitAndAssertTraces(
167+
trace ->
168+
trace.hasSpansSatisfyingExactly(
169+
span -> {
170+
assertServerSpan(span, "GET", DEFERRED_RESULT, DEFERRED_RESULT.getStatus());
171+
span.hasNoParent();
172+
},
173+
span ->
174+
assertHandlerSpan(span, "GET", DEFERRED_RESULT).hasParent(trace.getSpan(0)),
175+
span ->
176+
span.hasName("async-call-child")
177+
.hasKind(SpanKind.INTERNAL)
178+
.hasParent(trace.getSpan(1))
179+
.hasTotalAttributeCount(0),
180+
// Handler method runs once for the initial request and again for the async
181+
// redispatch when DeferredResult completes, so we expect two spans with the
182+
// same name. The second handler span is parented to the async child span.
183+
span ->
184+
assertHandlerSpan(span, "GET", DEFERRED_RESULT)
185+
.hasParent(trace.getSpan(2))));
186+
}
187+
147188
@Override
148189
protected List<Consumer<SpanDataAssert>> errorPageSpanAssertions(
149190
String method, ServerEndpoint endpoint) {
@@ -228,6 +269,8 @@ private static String getHandlerSpanName(ServerEndpoint endpoint) {
228269
return "TestController.captureHeaders";
229270
} else if (INDEXED_CHILD.equals(endpoint)) {
230271
return "TestController.indexedChild";
272+
} else if (DEFERRED_RESULT.equals(endpoint)) {
273+
return "TestController.deferredResult";
231274
}
232275
return "TestController." + endpoint.name().toLowerCase(Locale.ROOT);
233276
}

instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package io.opentelemetry.instrumentation.spring.webmvc.boot;
77

88
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.scheduling.annotation.EnableAsync;
910

1011
@SpringBootApplication
12+
@EnableAsync
1113
public class AppConfig {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.webmvc.boot;
7+
8+
import static io.opentelemetry.instrumentation.spring.webmvc.boot.AbstractSpringBootBasedTest.DEFERRED_RESULT;
9+
10+
import io.opentelemetry.api.GlobalOpenTelemetry;
11+
import io.opentelemetry.api.trace.Span;
12+
import io.opentelemetry.api.trace.Tracer;
13+
import io.opentelemetry.context.Scope;
14+
import org.springframework.scheduling.annotation.Async;
15+
import org.springframework.stereotype.Service;
16+
import org.springframework.web.context.request.async.DeferredResult;
17+
18+
@Service
19+
public class TestBean {
20+
21+
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test");
22+
23+
@Async
24+
public void asyncCall(DeferredResult<String> deferredResult) {
25+
Span span = tracer.spanBuilder("async-call-child").startSpan();
26+
try (Scope ignored = span.makeCurrent()) {
27+
deferredResult.setResult(DEFERRED_RESULT.getBody());
28+
} finally {
29+
span.end();
30+
}
31+
}
32+
}

instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS;
1717

1818
import java.util.Objects;
19+
import org.springframework.beans.factory.annotation.Autowired;
1920
import org.springframework.http.HttpStatus;
2021
import org.springframework.http.ResponseEntity;
2122
import org.springframework.stereotype.Controller;
@@ -25,11 +26,14 @@
2526
import org.springframework.web.bind.annotation.RequestMapping;
2627
import org.springframework.web.bind.annotation.RequestParam;
2728
import org.springframework.web.bind.annotation.ResponseBody;
29+
import org.springframework.web.context.request.async.DeferredResult;
2830
import org.springframework.web.servlet.view.RedirectView;
2931

3032
@Controller
3133
public class TestController {
3234

35+
@Autowired private TestBean testBean;
36+
3337
@RequestMapping("/basicsecured/endpoint")
3438
@ResponseBody
3539
String secureEndpoint() {
@@ -100,6 +104,14 @@ String indexedChild(@RequestParam("id") String id) {
100104
});
101105
}
102106

107+
@RequestMapping("/deferred-result")
108+
@ResponseBody
109+
DeferredResult<String> deferredResult() {
110+
DeferredResult<String> deferredResult = new DeferredResult<>();
111+
testBean.asyncCall(deferredResult);
112+
return deferredResult;
113+
}
114+
103115
@ExceptionHandler
104116
ResponseEntity<String> handleException(Throwable throwable) {
105117
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value())

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public void configure(IgnoredTypesBuilder builder) {
5353

5454
builder
5555
.ignoreClass("org.springframework.aop.")
56+
.allowClass("org.springframework.aop.interceptor.AsyncExecutionInterceptor$")
5657
.ignoreClass("org.springframework.cache.")
5758
.ignoreClass("org.springframework.dao.")
5859
.ignoreClass("org.springframework.ejb.")

0 commit comments

Comments
 (0)