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..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 @@ -59,4 +59,11 @@ protected void configure(HttpServerTestOptions options) { Boolean.getBoolean("testLatestDeps") ? 500 : 200); options.setExpectedException(new RuntimeException(EXCEPTION.getBody())); } + + @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"); + } } 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..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 @@ -22,6 +22,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; @@ -51,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"; @@ -58,6 +62,10 @@ public abstract class AbstractSpringBootBasedTest protected abstract Class securityConfigClass(); + protected boolean shouldTestDeferredResult() { + return true; + } + @Override protected void stopServer(ConfigurableApplicationContext ctx) { ctx.close(); @@ -144,6 +152,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("async-call-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 +269,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..f4f05b777227 --- /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.spring.webmvc.boot.AbstractSpringBootBasedTest.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 asyncCall(DeferredResult deferredResult) { + Span span = tracer.spanBuilder("async-call-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..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 @@ -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.asyncCall(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.")