From 74b1117e4280632b32cd313f5c1381184fc3bd5b Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 14 Nov 2025 10:29:08 -0800 Subject: [PATCH 1/4] Add test for Spring @EnableAsync --- .../webmvc/v3_1/boot/SpringBootBasedTest.java | 5 +++ .../boot/AbstractSpringBootBasedTest.java | 41 +++++++++++++++++++ .../spring/webmvc/boot/AppConfig.java | 2 + .../spring/webmvc/boot/TestBean.java | 32 +++++++++++++++ .../spring/webmvc/boot/TestController.java | 12 ++++++ ...ditionalLibraryIgnoredTypesConfigurer.java | 1 + .../testing/junit/http/ServerEndpoint.java | 2 + 7 files changed, 95 insertions(+) create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java index 2de935fc3553..4e4dedadfd32 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java @@ -59,4 +59,9 @@ protected void configure(HttpServerTestOptions options) { Boolean.getBoolean("testLatestDeps") ? 500 : 200); options.setExpectedException(new RuntimeException(EXCEPTION.getBody())); } + + @Override + protected boolean shouldTestDeferredResult() { + return Boolean.getBoolean("testLatestDeps"); + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java index f4ec82ce4122..1cb1f6903057 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java @@ -9,6 +9,7 @@ import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionSuffixAssertions; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.DEFERRED_RESULT; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN; @@ -22,6 +23,7 @@ import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanKind; @@ -58,6 +60,10 @@ public abstract class AbstractSpringBootBasedTest protected abstract Class securityConfigClass(); + protected boolean shouldTestDeferredResult() { + return true; + } + @Override protected void stopServer(ConfigurableApplicationContext ctx) { ctx.close(); @@ -144,6 +150,39 @@ void testCharacterEncodingOfTestPassword(String testPassword) { .hasKind(SpanKind.INTERNAL))); } + @Test + void deferredResult() { + assumeTrue(shouldTestDeferredResult()); + + AggregatedHttpResponse response = + client.execute(request(DEFERRED_RESULT, "GET")).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(DEFERRED_RESULT.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(DEFERRED_RESULT.getBody()); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + assertServerSpan(span, "GET", DEFERRED_RESULT, DEFERRED_RESULT.getStatus()); + span.hasNoParent(); + }, + span -> + assertHandlerSpan(span, "GET", DEFERRED_RESULT).hasParent(trace.getSpan(0)), + span -> + span.hasName("deferred-result-child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)) + .hasTotalAttributeCount(0), + // Handler method runs once for the initial request and again for the async + // redispatch when DeferredResult completes, so we expect two spans with the + // same name. The second handler span is parented to the async child span. + span -> + assertHandlerSpan(span, "GET", DEFERRED_RESULT) + .hasParent(trace.getSpan(2)))); + } + @Override protected List> errorPageSpanAssertions( String method, ServerEndpoint endpoint) { @@ -228,6 +267,8 @@ private static String getHandlerSpanName(ServerEndpoint endpoint) { return "TestController.captureHeaders"; } else if (INDEXED_CHILD.equals(endpoint)) { return "TestController.indexedChild"; + } else if (DEFERRED_RESULT.equals(endpoint)) { + return "TestController.deferredResult"; } return "TestController." + endpoint.name().toLowerCase(Locale.ROOT); } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java index a63e9b7d9187..648ff6726527 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java @@ -6,6 +6,8 @@ package io.opentelemetry.instrumentation.spring.webmvc.boot; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class AppConfig {} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java new file mode 100644 index 000000000000..aaf4f399f52d --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.boot; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.DEFERRED_RESULT; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.async.DeferredResult; + +@Service +public class TestBean { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + @Async + public void asyncDependencyCall(DeferredResult deferredResult) { + Span span = tracer.spanBuilder("deferred-result-child").startSpan(); + try (Scope ignored = span.makeCurrent()) { + deferredResult.setResult(DEFERRED_RESULT.getBody()); + } finally { + span.end(); + } + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java index ffc8c3e26e0e..73239889fff8 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java @@ -16,6 +16,7 @@ import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; import java.util.Objects; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -25,11 +26,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.servlet.view.RedirectView; @Controller public class TestController { + @Autowired private TestBean testBean; + @RequestMapping("/basicsecured/endpoint") @ResponseBody String secureEndpoint() { @@ -100,6 +104,14 @@ String indexedChild(@RequestParam("id") String id) { }); } + @RequestMapping("/deferred-result") + @ResponseBody + DeferredResult deferredResult() { + DeferredResult deferredResult = new DeferredResult<>(); + testBean.asyncDependencyCall(deferredResult); + return deferredResult; + } + @ExceptionHandler ResponseEntity handleException(Throwable throwable) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()) diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java index 02e4c5039cd3..7b51eeb3ad0c 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java @@ -53,6 +53,7 @@ public void configure(IgnoredTypesBuilder builder) { builder .ignoreClass("org.springframework.aop.") + .allowClass("org.springframework.aop.interceptor.AsyncExecutionInterceptor$") .ignoreClass("org.springframework.cache.") .ignoreClass("org.springframework.dao.") .ignoreClass("org.springframework.ejb.") diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java index ced8475fcc05..2f6a604afcfa 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java @@ -48,6 +48,8 @@ public class ServerEndpoint { new ServerEndpoint("AUTH_ERROR", "basicsecured/endpoint", 401, null); public static final ServerEndpoint INDEXED_CHILD = new ServerEndpoint("INDEXED_CHILD", "child", 200, "success"); + public static final ServerEndpoint DEFERRED_RESULT = + new ServerEndpoint("DEFERRED_RESULT", "deferred-result", 200, "deferred result"); public static final String ID_ATTRIBUTE_NAME = "test.request.id"; public static final String ID_PARAMETER_NAME = "id"; From 1e8900f923df8da6a62966b556e7659f3d6dbcd4 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 14 Nov 2025 15:38:53 -0800 Subject: [PATCH 2/4] simplify --- .../spring/webmvc/boot/AbstractSpringBootBasedTest.java | 6 ++++-- .../instrumentation/spring/webmvc/boot/TestBean.java | 6 +++--- .../instrumentation/spring/webmvc/boot/TestController.java | 2 +- .../instrumentation/testing/junit/http/ServerEndpoint.java | 2 -- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java index 1cb1f6903057..0c99e4606360 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java @@ -9,7 +9,6 @@ import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionSuffixAssertions; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.DEFERRED_RESULT; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN; @@ -53,6 +52,9 @@ public abstract class AbstractSpringBootBasedTest extends AbstractHttpServerTest { + static final ServerEndpoint DEFERRED_RESULT = + new ServerEndpoint("DEFERRED_RESULT", "deferred-result", 200, "deferred result"); + private static final String EXPERIMENTAL_SPAN_CONFIG = "otel.instrumentation.spring-webmvc.experimental-span-attributes"; @@ -171,7 +173,7 @@ void deferredResult() { span -> assertHandlerSpan(span, "GET", DEFERRED_RESULT).hasParent(trace.getSpan(0)), span -> - span.hasName("deferred-result-child") + span.hasName("async-call-child") .hasKind(SpanKind.INTERNAL) .hasParent(trace.getSpan(1)) .hasTotalAttributeCount(0), diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java index aaf4f399f52d..f4f05b777227 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestBean.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.spring.webmvc.boot; -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.DEFERRED_RESULT; +import static io.opentelemetry.instrumentation.spring.webmvc.boot.AbstractSpringBootBasedTest.DEFERRED_RESULT; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; @@ -21,8 +21,8 @@ public class TestBean { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); @Async - public void asyncDependencyCall(DeferredResult deferredResult) { - Span span = tracer.spanBuilder("deferred-result-child").startSpan(); + public void asyncCall(DeferredResult deferredResult) { + Span span = tracer.spanBuilder("async-call-child").startSpan(); try (Scope ignored = span.makeCurrent()) { deferredResult.setResult(DEFERRED_RESULT.getBody()); } finally { diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java index 73239889fff8..2e4966926d38 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java @@ -108,7 +108,7 @@ String indexedChild(@RequestParam("id") String id) { @ResponseBody DeferredResult deferredResult() { DeferredResult deferredResult = new DeferredResult<>(); - testBean.asyncDependencyCall(deferredResult); + testBean.asyncCall(deferredResult); return deferredResult; } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java index 2f6a604afcfa..ced8475fcc05 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java @@ -48,8 +48,6 @@ public class ServerEndpoint { new ServerEndpoint("AUTH_ERROR", "basicsecured/endpoint", 401, null); public static final ServerEndpoint INDEXED_CHILD = new ServerEndpoint("INDEXED_CHILD", "child", 200, "success"); - public static final ServerEndpoint DEFERRED_RESULT = - new ServerEndpoint("DEFERRED_RESULT", "deferred-result", 200, "deferred result"); public static final String ID_ATTRIBUTE_NAME = "test.request.id"; public static final String ID_PARAMETER_NAME = "id"; From f875175d3b71a7a6548a6bbcb297cea2dadb26de Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 14 Nov 2025 16:39:04 -0800 Subject: [PATCH 3/4] comment --- .../spring/webmvc/v3_1/boot/SpringBootBasedTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java index 4e4dedadfd32..a6b72e26e123 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java @@ -55,6 +55,7 @@ public Class securityConfigClass() { @Override protected void configure(HttpServerTestOptions options) { super.configure(options); + // older versions of Spring Boot return 200 for non-standard HTTP methods instead of 500 options.setResponseCodeOnNonStandardHttpMethod( Boolean.getBoolean("testLatestDeps") ? 500 : 200); options.setExpectedException(new RuntimeException(EXCEPTION.getBody())); @@ -62,6 +63,8 @@ protected void configure(HttpServerTestOptions options) { @Override protected boolean shouldTestDeferredResult() { + // older versions of Spring Boot don't properly propagate context to async calls, + // resulting in a separate trace instead of a single trace return Boolean.getBoolean("testLatestDeps"); } } From ccf47b685dd3079d37a8af9d81e6370f8d4e71e1 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 14 Nov 2025 17:22:42 -0800 Subject: [PATCH 4/4] Apply suggestion from @trask --- .../spring/webmvc/v3_1/boot/SpringBootBasedTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java index a6b72e26e123..76bc6d073b77 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java @@ -55,7 +55,6 @@ public Class securityConfigClass() { @Override protected void configure(HttpServerTestOptions options) { super.configure(options); - // older versions of Spring Boot return 200 for non-standard HTTP methods instead of 500 options.setResponseCodeOnNonStandardHttpMethod( Boolean.getBoolean("testLatestDeps") ? 500 : 200); options.setExpectedException(new RuntimeException(EXCEPTION.getBody()));