Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,13 +52,20 @@
public abstract class AbstractSpringBootBasedTest
extends AbstractHttpServerTest<ConfigurableApplicationContext> {

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";

protected abstract ConfigurableApplicationContext context();

protected abstract Class<?> securityConfigClass();

protected boolean shouldTestDeferredResult() {
return true;
}

@Override
protected void stopServer(ConfigurableApplicationContext ctx) {
ctx.close();
Expand Down Expand Up @@ -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<Consumer<SpanDataAssert>> errorPageSpanAssertions(
String method, ServerEndpoint endpoint) {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
@@ -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<String> deferredResult) {
Span span = tracer.spanBuilder("async-call-child").startSpan();
try (Scope ignored = span.makeCurrent()) {
deferredResult.setResult(DEFERRED_RESULT.getBody());
} finally {
span.end();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -100,6 +104,14 @@ String indexedChild(@RequestParam("id") String id) {
});
}

@RequestMapping("/deferred-result")
@ResponseBody
DeferredResult<String> deferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>();
testBean.asyncCall(deferredResult);
return deferredResult;
}

@ExceptionHandler
ResponseEntity<String> handleException(Throwable throwable) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
Loading