diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts index 8dd2bf8eabc1..2d6f7d1db069 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts @@ -60,10 +60,9 @@ dependencies { testLibrary("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") testLibrary("org.springframework.boot:spring-boot-starter-reactor-netty:2.0.0.RELEASE") - // tests don't work with spring boot 4 yet - latestDepTestLibrary("org.springframework.boot:spring-boot-starter-webflux:3.+") // documented limitation - latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // documented limitation - latestDepTestLibrary("org.springframework.boot:spring-boot-starter-reactor-netty:3.+") // documented limitation + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-webflux:3.+") // see testing-webflux7 module + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // see testing-webflux7 module + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-reactor-netty:3.+") // see testing-webflux7 module } val latestDepTest = findProperty("testLatestDeps") as Boolean diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java index 6a0294a5a2c6..4e7941213fe0 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java @@ -5,67 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server; -import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionAssertions; -import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionPrefixAssertions; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS; -import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; -import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; -import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; -import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS; -import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT; -import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; -import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; -import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; -import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; -import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; -import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; -import static org.junit.jupiter.api.Named.named; - -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import io.opentelemetry.sdk.testing.assertj.EventDataAssert; -import io.opentelemetry.sdk.testing.assertj.TraceAssert; -import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.testing.internal.armeria.client.WebClient; -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; -import io.opentelemetry.testing.internal.armeria.common.HttpStatus; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractSpringWebfluxTest; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.test.context.junit.jupiter.SpringExtension; -import server.EchoHandlerFunction; -import server.FooModel; import server.SpringWebFluxTestApplication; -import server.TestController; -@SuppressWarnings("deprecation") // using deprecated semconv @ExtendWith(SpringExtension.class) @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -73,7 +21,7 @@ SpringWebFluxTestApplication.class, SpringWebfluxTest.ForceNettyAutoConfiguration.class }) -class SpringWebfluxTest { +class SpringWebfluxTest extends AbstractSpringWebfluxTest { @TestConfiguration static class ForceNettyAutoConfiguration { @Bean @@ -81,691 +29,4 @@ NettyReactiveWebServerFactory nettyFactory() { return new NettyReactiveWebServerFactory(); } } - - @RegisterExtension - static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - private static final String INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX = - SpringWebFluxTestApplication.class.getName() + "$"; - private static final String SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX = - SpringWebFluxTestApplication.class.getSimpleName() + "$"; - - // can't use @LocalServerPort annotation since it moved packages between Spring Boot 2 and 3 - @Value("${local.server.port}") - private int port; - - private WebClient client; - - @BeforeEach - void beforeEach() { - client = WebClient.builder("h1c://localhost:" + port).followRedirects().build(); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("provideParameters") - void basicGetTest(Parameter parameter) { - AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); - - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET " + parameter.urlPathWithVariables) - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, parameter.urlPath), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), - span -> { - if (parameter.annotatedMethod == null) { - // Functional API - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - } else { - // Annotation API - span.hasName( - TestController.class.getSimpleName() + "." + parameter.annotatedMethod); - } - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); - })); - } - - private static List assertCodeFunction(Parameter parameter) { - String expectedFunctionName = - parameter.annotatedMethod == null ? "handle" : parameter.annotatedMethod; - String expectedPrefix = - parameter.annotatedMethod == null - ? INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX - : TestController.class.getName(); - - return codeFunctionPrefixAssertions(expectedPrefix, expectedFunctionName); - } - - private static Stream provideParameters() { - return Stream.of( - Arguments.of( - named( - "functional API without parameters", - new Parameter( - "/greet", - "/greet", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), - Arguments.of( - named( - "functional API with one parameter", - new Parameter( - "/greet/WORLD", - "/greet/{name}", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " WORLD"))), - Arguments.of( - named( - "functional API with two parameters", - new Parameter( - "/greet/World/Test1", - "/greet/{name}/{word}", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE - + " World Test1"))), - Arguments.of( - named( - "functional API delayed response", - new Parameter( - "/greet-delayed", - "/greet-delayed", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), - Arguments.of( - named( - "annotation API without parameters", - new Parameter( - "/foo", "/foo", "getFooModel", new FooModel(0L, "DEFAULT").toString()))), - Arguments.of( - named( - "annotation API with one parameter", - new Parameter( - "/foo/1", "/foo/{id}", "getFooModel", new FooModel(1L, "pass").toString()))), - Arguments.of( - named( - "annotation API with two parameters", - new Parameter( - "/foo/2/world", - "/foo/{id}/{name}", - "getFooModel", - new FooModel(2L, "world").toString()))), - Arguments.of( - named( - "annotation API delayed response", - new Parameter( - "/foo-delayed", - "/foo-delayed", - "getFooDelayed", - new FooModel(3L, "delayed").toString()))), - Arguments.of( - named( - "annotation API without parameters no mono", - new Parameter( - "/foo-no-mono", - "/foo-no-mono", - "getFooModelNoMono", - new FooModel(0L, "DEFAULT").toString())))); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("provideAsyncParameters") - void getAsyncResponseTest(Parameter parameter) { - AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); - - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET " + parameter.urlPathWithVariables) - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, parameter.urlPath), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), - span -> { - if (parameter.annotatedMethod == null) { - // Functional API - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - } else { - // Annotation API - span.hasName( - TestController.class.getSimpleName() + "." + parameter.annotatedMethod); - } - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); - }, - span -> - span.hasName("tracedMethod") - .hasParent(trace.getSpan(1)) - .hasTotalAttributeCount(0))); - } - - private static Stream provideAsyncParameters() { - return Stream.of( - Arguments.of( - named( - "functional API traced method from mono", - new Parameter( - "/greet-mono-from-callable/4", - "/greet-mono-from-callable/{id}", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 4"))), - Arguments.of( - named( - "functional API traced method with delay", - new Parameter( - "/greet-delayed-mono/6", - "/greet-delayed-mono/{id}", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 6"))), - Arguments.of( - named( - "annotation API traced method from mono", - new Parameter( - "/foo-mono-from-callable/7", - "/foo-mono-from-callable/{id}", - "getMonoFromCallable", - new FooModel(7L, "tracedMethod").toString()))), - Arguments.of( - named( - "annotation API traced method with delay", - new Parameter( - "/foo-delayed-mono/9", - "/foo-delayed-mono/{id}", - "getFooDelayedMono", - new FooModel(9L, "tracedMethod").toString())))); - } - - /* - This test differs from the previous in one important aspect. - The test above calls endpoints which does not create any spans during their invocation. - They merely assemble reactive pipeline where some steps create spans. - Thus all those spans are created when WebFlux span created by DispatcherHandlerInstrumentation - has already finished. Therefore, they have `SERVER` span as their parent. - - This test below calls endpoints which do create spans right inside endpoint handler. - Therefore, in theory, those spans should have INTERNAL span created by DispatcherHandlerInstrumentation - as their parent. But there is a difference how Spring WebFlux handles functional endpoints - (created in server.SpringWebFluxTestApplication.greetRouterFunction) and annotated endpoints - (created in server.TestController). - In the former case org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle - calls handler function directly. Thus "tracedMethod" span below has INTERNAL handler span as its parent. - In the latter case org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.handle - merely wraps handler call into Mono and thus actual invocation of handler function happens later, - when INTERNAL handler span has already finished. Thus, "tracedMethod" has SERVER Netty span as its parent. - */ - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("provideAsyncHandlerFuncParameters") - void createSpanDuringHandlerFunctionTest(Parameter parameter) { - AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); - - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET " + parameter.urlPathWithVariables) - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, parameter.urlPath), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), - span -> { - if (parameter.annotatedMethod == null) { - // Functional API - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - } else { - // Annotation API - span.hasName( - TestController.class.getSimpleName() + "." + parameter.annotatedMethod); - } - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); - }, - span -> - span.hasName("tracedMethod") - .hasParent(trace.getSpan(1)) - .hasTotalAttributeCount(0))); - } - - private static Stream provideAsyncHandlerFuncParameters() { - return Stream.of( - Arguments.of( - named( - "functional API traced method", - new Parameter( - "/greet-traced-method/5", - "/greet-traced-method/{id}", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 5"))), - Arguments.of( - named( - "annotation API traced method", - new Parameter( - "/foo-traced-method/8", - "/foo-traced-method/{id}", - "getTracedMethod", - new FooModel(8L, "tracedMethod").toString())))); - } - - @Test - void get404Test() { - AggregatedHttpResponse response = client.get("/notfoundgreet").aggregate().join(); - - assertThat(response.status().code()).isEqualTo(404); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET /**") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasStatus(StatusData.unset()) - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, "/notfoundgreet"), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 404), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/**")), - span -> - span.hasName("ResourceWebHandler.handle") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) - .hasEventsSatisfyingExactly(SpringWebfluxTest::resource404Exception) - .hasAttributesSatisfyingExactly( - codeFunctionAssertions( - "org.springframework.web.reactive.resource.ResourceWebHandler", - "handle")))); - } - - private static void resource404Exception(EventDataAssert event) { - if (Boolean.getBoolean("testLatestDeps")) { - event - .hasName("exception") - .hasAttributesSatisfyingExactly( - equalTo( - EXCEPTION_TYPE, - "org.springframework.web.reactive.resource.NoResourceFoundException"), - satisfies(EXCEPTION_MESSAGE, val -> val.isInstanceOf(String.class)), - satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); - } else { - event - .hasName("exception") - .hasAttributesSatisfyingExactly( - equalTo(EXCEPTION_TYPE, "org.springframework.web.server.ResponseStatusException"), - equalTo(EXCEPTION_MESSAGE, "Response status 404"), - satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); - } - } - - @Test - void basicPostTest() { - String echoString = "TEST"; - AggregatedHttpResponse response = client.post("/echo", echoString).aggregate().join(); - - assertThat(response.status().code()).isEqualTo(202); - assertThat(response.contentUtf8()).isEqualTo(echoString); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST /echo") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, "/echo"), - equalTo(HTTP_REQUEST_METHOD, "POST"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 202), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/echo")), - span -> - span.hasName(EchoHandlerFunction.class.getSimpleName() + ".handle") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - codeFunctionAssertions(EchoHandlerFunction.class, "handle")), - span -> - span.hasName("echo").hasParent(trace.getSpan(1)).hasTotalAttributeCount(0))); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("provideBadEndpointParameters") - void getToBadEndpointTest(Parameter parameter) { - AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); - - assertThat(response.status().code()).isEqualTo(500); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET " + parameter.urlPathWithVariables) - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasStatus(StatusData.error()) - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, parameter.urlPath), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 500), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), - equalTo(ERROR_TYPE, "500")), - span -> { - if (parameter.annotatedMethod == null) { - // Functional API - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - } else { - // Annotation API - span.hasName( - TestController.class.getSimpleName() + "." + parameter.annotatedMethod); - } - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) - .hasEventsSatisfyingExactly( - event -> - event - .hasName("exception") - .hasAttributesSatisfyingExactly( - equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), - equalTo(EXCEPTION_MESSAGE, "bad things happen"), - satisfies( - EXCEPTION_STACKTRACE, - val -> val.isInstanceOf(String.class)))) - .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); - })); - } - - private static Stream provideBadEndpointParameters() { - return Stream.of( - Arguments.of( - named( - "functional API fail fast", - new Parameter("/greet-failfast/1", "/greet-failfast/{id}", null, null))), - Arguments.of( - named( - "functional API fail Mono", - new Parameter("/greet-failmono/1", "/greet-failmono/{id}", null, null))), - Arguments.of( - named( - "annotation API fail fast", - new Parameter("/foo-failfast/1", "/foo-failfast/{id}", "getFooFailFast", null))), - Arguments.of( - named( - "annotation API fail Mono", - new Parameter("/foo-failmono/1", "/foo-failmono/{id}", "getFooFailMono", null)))); - } - - @Test - void redirectTest() { - AggregatedHttpResponse response = client.get("/double-greet-redirect").aggregate().join(); - - assertThat(response.status().code()).isEqualTo(200); - testing.waitAndAssertTraces( - trace -> - // TODO: why order of spans is different in these traces? - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET /double-greet-redirect") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, "/double-greet-redirect"), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 307), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/double-greet-redirect")), - span -> - span.hasName("RedirectComponent$$Lambda.handle") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - codeFunctionPrefixAssertions( - "server.RedirectComponent$$Lambda", "handle"))), - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET /double-greet") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, "/double-greet"), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/double-greet")), - span -> { - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - codeFunctionPrefixAssertions( - INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX, "handle")); - })); - } - - @ParameterizedTest(name = "{index}: {0}") - @MethodSource("provideMultipleDelayingRouteParameters") - void multipleGetsToDelayingRoute(Parameter parameter) { - int requestsCount = 50; - - List responses = - IntStream.rangeClosed(0, requestsCount - 1) - .mapToObj(n -> client.get(parameter.urlPath).aggregate().join()) - .collect(Collectors.toList()); - - assertThat(responses) - .extracting(AggregatedHttpResponse::status) - .extracting(HttpStatus::code) - .containsOnly(200); - assertThat(responses) - .extracting(AggregatedHttpResponse::contentUtf8) - .containsOnly(parameter.expectedResponseBody); - - Consumer traceAssertion = - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET " + parameter.urlPathWithVariables) - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, parameter.urlPath), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), - span -> { - if (parameter.annotatedMethod == null) { - // Functional API - assertThat(trace.getSpan(1).getName()) - .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); - } else { - // Annotation API - span.hasName( - TestController.class.getSimpleName() + "." + parameter.annotatedMethod); - } - span.hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); - }); - - testing.waitAndAssertTraces(Collections.nCopies(requestsCount, traceAssertion)); - } - - private static Stream provideMultipleDelayingRouteParameters() { - return Stream.of( - Arguments.of( - named( - "functional API delayed response", - new Parameter( - "/greet-delayed", - "/greet-delayed", - null, - SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), - Arguments.of( - named( - "annotation API delayed response", - new Parameter( - "/foo-delayed", - "/foo-delayed", - "getFooDelayed", - new FooModel(3L, "delayed").toString())))); - } - - @Test - void cancelRequestTest() throws Exception { - // fails with SingleThreadedSpringWebfluxTest - Assumptions.assumeTrue(this.getClass() == SpringWebfluxTest.class); - - WebClient client = - WebClient.builder("h1c://localhost:" + port) - .responseTimeout(Duration.ofSeconds(1)) - .followRedirects() - .build(); - try { - client.get("/slow").aggregate().get(); - } catch (ExecutionException ignore) { - // ignore - } - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET /slow") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasStatus(StatusData.unset()) - .hasAttributesSatisfyingExactly( - equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), - equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), - satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(SERVER_ADDRESS, "localhost"), - satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(CLIENT_ADDRESS, "127.0.0.1"), - equalTo(URL_PATH, "/slow"), - equalTo(HTTP_REQUEST_METHOD, "GET"), - equalTo(URL_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/slow"), - equalTo(ERROR_TYPE, "_OTHER")), - span -> - span.hasName("SpringWebFluxTestApplication$$Lambda.handle") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - codeFunctionPrefixAssertions( - "server.SpringWebFluxTestApplication$$Lambda", "handle")))); - - SpringWebFluxTestApplication.resumeSlowRequest(); - } - - private static class Parameter { - final String urlPath; - final String urlPathWithVariables; - final String annotatedMethod; - final String expectedResponseBody; - - Parameter( - String urlPath, - String urlPathWithVariables, - String annotatedMethod, - String expectedResponseBody) { - this.urlPath = urlPath; - this.urlPathWithVariables = urlPathWithVariables; - this.annotatedMethod = annotatedMethod; - this.expectedResponseBody = expectedResponseBody; - } - } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java index cf199a549bb1..43b3123efa26 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractControllerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.spring.webflux.server.ServerTestController; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.time.Duration; import java.util.function.Supplier; @@ -20,7 +22,7 @@ * within a Mono map step, which follows a delay step. For exception endpoint, the exception is * thrown within the last map step. */ -class DelayedControllerSpringWebFluxServerTest extends ControllerSpringWebFluxServerTest { +class DelayedControllerSpringWebFluxServerTest extends AbstractControllerSpringWebFluxServerTest { @Override protected Class getApplicationClass() { return Application.class; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java index f956798dc314..650ffd4c8e3f 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractHandlerSpringWebFluxServerTest; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.time.Duration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -20,7 +21,7 @@ * map step, which follows a delay step. For exception endpoint, the exception is thrown within the * last map step. */ -class DelayedHandlerSpringWebFluxServerTest extends HandlerSpringWebFluxServerTest { +class DelayedHandlerSpringWebFluxServerTest extends AbstractHandlerSpringWebFluxServerTest { @Override protected Class getApplicationClass() { return Application.class; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java index a029ffe587c3..794c0309b784 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractControllerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.spring.webflux.server.ServerTestController; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.util.function.Supplier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -20,7 +22,7 @@ *

{@code Mono} from a handler is already a fully constructed response with no deferred * actions. For exception endpoint, the exception is thrown within controller method scope. */ -class ImmediateControllerSpringWebFluxServerTest extends ControllerSpringWebFluxServerTest { +class ImmediateControllerSpringWebFluxServerTest extends AbstractControllerSpringWebFluxServerTest { @Override protected Class getApplicationClass() { return Application.class; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java index 63b0a03eaa45..248bb7f00038 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java @@ -5,13 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractImmediateHandlerSpringWebFluxServerTest; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; -import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.context.annotation.Bean; @@ -20,14 +15,8 @@ import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; -/** - * Tests the case where "controller" span is created within the route handler method scope, and the - * - *

{@code Mono} from a handler is already a fully constructed response with no - * deferred actions. For exception endpoint, the exception is thrown within route handler method - * scope. - */ -class ImmediateHandlerSpringWebFluxServerTest extends HandlerSpringWebFluxServerTest { +class ImmediateHandlerSpringWebFluxServerTest + extends AbstractImmediateHandlerSpringWebFluxServerTest { @Override protected Class getApplicationClass() { return Application.class; @@ -60,18 +49,4 @@ protected Mono wrapResponse( }); } } - - @Test - void nestedPath() { - assumeTrue(Boolean.getBoolean("testLatestDeps")); - - String method = "GET"; - AggregatedHttpRequest request = request(NESTED_PATH, method); - AggregatedHttpResponse response = client.execute(request).aggregate().join(); - assertThat(response.status().code()).isEqualTo(NESTED_PATH.getStatus()); - assertThat(response.contentUtf8()).isEqualTo(NESTED_PATH.getBody()); - assertResponseHasCustomizedHeaders(response, NESTED_PATH, null); - - assertTheTraces(1, null, null, null, method, NESTED_PATH); - } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java index 7d91a7aa48b4..dd176682c19b 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; -import static io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base.SpringWebFluxServerTest.NESTED_PATH; +import static io.opentelemetry.instrumentation.spring.webflux.server.AbstractSpringWebFluxServerTest.NESTED_PATH; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.path; import static org.springframework.web.reactive.function.server.RouterFunctions.nest; @@ -106,12 +106,7 @@ public RouterFunction createRoutes() { path("/nestedPath"), nest( path("/hello"), - route( - path("/world"), - request -> { - ServerEndpoint endpoint = NESTED_PATH; - return respond(endpoint, null, null, null); - }))); + route(path("/world"), request -> respond(NESTED_PATH, null, null, null)))); } protected Mono respond( diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/build.gradle.kts new file mode 100644 index 000000000000..f0b4dc1ba9da --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testInstrumentation(project(":instrumentation:spring:spring-core-2.0:javaagent")) + testInstrumentation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent")) + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent")) + + testImplementation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:testing")) + testImplementation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing")) + + testImplementation("org.springframework.boot:spring-boot-starter-webflux:4.0.0") + testImplementation("org.springframework:spring-web:7.0.0") + testImplementation("org.springframework.boot:spring-boot-starter-test:4.0.0") + testImplementation("org.springframework.boot:spring-boot-starter-reactor-netty:4.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/client/SpringWebfluxClientInstrumentationTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/client/SpringWebfluxClientInstrumentationTest.java new file mode 100644 index 000000000000..b67fabb15c21 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/client/SpringWebfluxClientInstrumentationTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.client; + +import io.opentelemetry.instrumentation.spring.webflux.client.AbstractSpringWebfluxClientInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.web.reactive.function.client.WebClient; + +class SpringWebfluxClientInstrumentationTest + extends AbstractSpringWebfluxClientInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + @Override + protected WebClient.Builder instrument(WebClient.Builder builder) { + return builder; + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + // Disable remote connection tests on Windows due to reactor-netty creating extra spans + if (OS.WINDOWS.isCurrentOs()) { + optionsBuilder.setTestRemoteConnection(false); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringThreadedSpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringThreadedSpringWebfluxTest.java new file mode 100644 index 000000000000..69ba5c9e29fe --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringThreadedSpringWebfluxTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server; + +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import reactor.netty.resources.LoopResources; +import server.SpringWebFluxTestApplication; + +/** + * Run all Webflux tests under netty event loop having only 1 thread. Some of the bugs are better + * visible in this setup because same thread is reused for different requests. + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + SpringWebFluxTestApplication.class, + SpringThreadedSpringWebfluxTest.ForceSingleThreadedNettyAutoConfiguration.class + }) +class SpringThreadedSpringWebfluxTest extends SpringWebfluxTest { + + @TestConfiguration + static class ForceSingleThreadedNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory(); + // Configure single-threaded event loop for Spring Boot 4 + factory.addServerCustomizers( + server -> server.runOn(LoopResources.create("my-http", 1, true))); + return factory; + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringWebfluxTest.java new file mode 100644 index 000000000000..7d25e82be658 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/SpringWebfluxTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server; + +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractSpringWebfluxTest; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import server.SpringWebFluxTestApplication; + +@ExtendWith(SpringExtension.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + SpringWebFluxTestApplication.class, + SpringWebfluxTest.ForceNettyAutoConfiguration.class + }) +class SpringWebfluxTest extends AbstractSpringWebfluxTest { + @TestConfiguration + static class ForceNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedControllerSpringWebFluxServerTest.java new file mode 100644 index 000000000000..202ce3492881 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedControllerSpringWebFluxServerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server.base; + +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractControllerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.spring.webflux.server.ServerTestController; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.time.Duration; +import java.util.function.Supplier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * Tests the case which uses annotated controller methods, and where "controller" span is created + * within a Mono map step, which follows a delay step. For exception endpoint, the exception is + * thrown within the last map step. + */ +class DelayedControllerSpringWebFluxServerTest extends AbstractControllerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application.class; + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + Controller controller() { + return new Controller(); + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + @RestController + static class Controller extends ServerTestController { + @Override + protected Mono wrapControllerMethod(ServerEndpoint endpoint, Supplier handler) { + return Mono.just("") + .delayElement(Duration.ofMillis(10)) + .map(unused -> controller(endpoint, handler::get)); + } + + @Override + protected void setStatus(ServerHttpResponse response, ServerEndpoint endpoint) { + response.setStatusCode(HttpStatusCode.valueOf(endpoint.getStatus())); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedHandlerSpringWebFluxServerTest.java new file mode 100644 index 000000000000..6e89b8e1234e --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/DelayedHandlerSpringWebFluxServerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server.base; + +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractHandlerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.time.Duration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * Tests the case which uses route handlers, and where "controller" span is created within a Mono + * map step, which follows a delay step. For exception endpoint, the exception is thrown within the + * last map step. + */ +class DelayedHandlerSpringWebFluxServerTest extends AbstractHandlerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application.class; + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + RouterFunction router() { + return new RouteFactory().createRoutes(); + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + static class RouteFactory extends ServerTestRouteFactory { + + @Override + protected Mono wrapResponse( + ServerEndpoint endpoint, Mono response, Runnable spanAction) { + return response + .delayElement(Duration.ofMillis(10)) + .map( + original -> + controller( + endpoint, + () -> { + spanAction.run(); + return original; + })); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateControllerSpringWebFluxServerTest.java new file mode 100644 index 000000000000..bb127e4d604c --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateControllerSpringWebFluxServerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server.base; + +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractControllerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.spring.webflux.server.ServerTestController; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.util.function.Supplier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * Tests the case where "controller" span is created within the controller method scope, and the + * + *

{@code Mono} from a handler is already a fully constructed response with no deferred + * actions. For exception endpoint, the exception is thrown within controller method scope. + */ +class ImmediateControllerSpringWebFluxServerTest extends AbstractControllerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application.class; + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + Controller controller() { + return new Controller(); + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + @RestController + static class Controller extends ServerTestController { + @Override + protected Mono wrapControllerMethod( + ServerEndpoint endpoint, Supplier controllerMethod) { + return Mono.just(controller(endpoint, controllerMethod::get)); + } + + @Override + protected void setStatus(ServerHttpResponse response, ServerEndpoint endpoint) { + response.setStatusCode(HttpStatusCode.valueOf(endpoint.getStatus())); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java new file mode 100644 index 000000000000..74cde956fd34 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server.base; + +import io.opentelemetry.instrumentation.spring.webflux.server.AbstractImmediateHandlerSpringWebFluxServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * Tests the case where {@code Mono} from a router function is already a fully + * constructed response with no deferred actions. For exception endpoint, the exception is thrown + * within router function scope. + */ +class ImmediateHandlerSpringWebFluxServerTest + extends AbstractImmediateHandlerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application.class; + } + + @Configuration + @EnableAutoConfiguration + static class Application { + + @Bean + RouterFunction router() { + return new RouteFactory().createRoutes(); + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + static class RouteFactory extends ServerTestRouteFactory { + @Override + protected Mono wrapResponse( + ServerEndpoint endpoint, Mono response, Runnable spanAction) { + return controller( + endpoint, + () -> { + spanAction.run(); + return response; + }); + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ServerTestRouteFactory.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ServerTestRouteFactory.java new file mode 100644 index 000000000000..a83eea47bf36 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing-webflux7/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v7_0/server/base/ServerTestRouteFactory.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v7_0.server.base; + +import static io.opentelemetry.instrumentation.spring.webflux.server.AbstractSpringWebFluxServerTest.NESTED_PATH; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.path; +import static org.springframework.web.reactive.function.server.RouterFunctions.nest; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; +import reactor.core.publisher.Mono; + +public abstract class ServerTestRouteFactory { + public RouterFunction createRoutes() { + return route( + GET("/success"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.SUCCESS; + + return respond(endpoint, null, null, null); + }) + .andRoute( + GET("/query"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.QUERY_PARAM; + + return respond(endpoint, null, request.uri().getRawQuery(), null); + }) + .andRoute( + GET("/redirect"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.REDIRECT; + + return respond( + endpoint, + ServerResponse.status(endpoint.getStatus()) + .header(HttpHeaders.LOCATION, endpoint.getBody()), + "", + null); + }) + .andRoute( + GET("/error-status"), + redirect -> { + ServerEndpoint endpoint = ServerEndpoint.ERROR; + + return respond(endpoint, null, null, null); + }) + .andRoute( + GET("/exception"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.EXCEPTION; + + return respond( + endpoint, + ServerResponse.ok(), + "", + () -> { + throw new IllegalStateException(endpoint.getBody()); + }); + }) + .andRoute( + GET("/path/{id}/param"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.PATH_PARAM; + + return respond(endpoint, null, request.pathVariable("id"), null); + }) + .andRoute( + GET("/child"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.INDEXED_CHILD; + + return respond( + endpoint, + null, + null, + () -> + Span.current() + .setAttribute( + "test.request.id", Long.parseLong(request.queryParam("id").get()))); + }) + .andRoute( + GET("/captureHeaders"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.CAPTURE_HEADERS; + + return respond( + endpoint, + ServerResponse.status(endpoint.getStatus()) + .header( + "X-Test-Response", + request.headers().asHttpHeaders().getFirst("X-Test-Request")), + null, + null); + }) + .andNest( + path("/nestedPath"), + nest( + path("/hello"), + route(path("/world"), request -> respond(NESTED_PATH, null, null, null)))); + } + + protected Mono respond( + ServerEndpoint endpoint, BodyBuilder bodyBuilder, String body, Runnable spanAction) { + if (bodyBuilder == null) { + bodyBuilder = ServerResponse.status(endpoint.getStatus()); + } + if (body == null) { + body = endpoint.getBody() != null ? endpoint.getBody() : ""; + } + if (spanAction == null) { + spanAction = () -> {}; + } + + return wrapResponse(endpoint, bodyBuilder.bodyValue(body), spanAction); + } + + protected abstract Mono wrapResponse( + ServerEndpoint endpoint, Mono response, Runnable spanAction); +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts index e3f5c23a436c..7682bc791998 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts @@ -8,4 +8,5 @@ dependencies { compileOnly("org.springframework:spring-webflux:5.0.0.RELEASE") compileOnly("org.springframework.boot:spring-boot-starter-reactor-netty:2.0.0.RELEASE") compileOnly("org.springframework.boot:spring-boot:2.0.0.RELEASE") + compileOnly("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractControllerSpringWebFluxServerTest.java similarity index 78% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractControllerSpringWebFluxServerTest.java index ea5c7be2bd1a..9f0f6bd0ef96 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractControllerSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +package io.opentelemetry.instrumentation.spring.webflux.server; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; @@ -12,6 +12,7 @@ import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; 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 io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; @@ -20,7 +21,8 @@ import io.opentelemetry.sdk.trace.data.StatusData; import java.util.Locale; -public abstract class ControllerSpringWebFluxServerTest extends SpringWebFluxServerTest { +public abstract class AbstractControllerSpringWebFluxServerTest + extends AbstractSpringWebFluxServerTest { @Override protected SpanDataAssert assertHandlerSpan( @@ -61,10 +63,20 @@ protected SpanDataAssert assertHandlerSpan( event .hasName("exception") .hasAttributesSatisfyingExactly( - equalTo( + satisfies( EXCEPTION_TYPE, - "org.springframework.web.server.ResponseStatusException"), - equalTo(EXCEPTION_MESSAGE, "Response status 404"), + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.server.ResponseStatusException"), + // Changed in spring 7+ + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.reactive.resource.NoResourceFoundException"))), + satisfies(EXCEPTION_MESSAGE, val -> val.contains("404")), satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractHandlerSpringWebFluxServerTest.java similarity index 75% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractHandlerSpringWebFluxServerTest.java index 637c5b8124ca..355e0ce7bb10 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractHandlerSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +package io.opentelemetry.instrumentation.spring.webflux.server; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; @@ -12,6 +12,7 @@ import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; 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 io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.api.internal.HttpConstants; @@ -20,12 +21,13 @@ import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.trace.data.StatusData; -public abstract class HandlerSpringWebFluxServerTest extends SpringWebFluxServerTest { +public abstract class AbstractHandlerSpringWebFluxServerTest + extends AbstractSpringWebFluxServerTest { @Override protected SpanDataAssert assertHandlerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { - String handlerSpanName = ServerTestRouteFactory.class.getSimpleName() + "$$Lambda.handle"; + String handlerSpanName = "ServerTestRouteFactory$$Lambda.handle"; if (endpoint == NOT_FOUND) { handlerSpanName = "ResourceWebHandler.handle"; } @@ -60,11 +62,21 @@ protected SpanDataAssert assertHandlerSpan( event .hasName("exception") .hasAttributesSatisfyingExactly( - equalTo( + satisfies( EXCEPTION_TYPE, - "org.springframework.web.server.ResponseStatusException"), - equalTo(EXCEPTION_MESSAGE, "Response status 404"), - satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); + val -> + val.satisfiesAnyOf( + // changed in spring 7+ + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.server.ResponseStatusException"), + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.reactive.resource.NoResourceFoundException"))), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)), + satisfies(EXCEPTION_MESSAGE, val -> val.contains("404")))); } } return span; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractImmediateHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractImmediateHandlerSpringWebFluxServerTest.java new file mode 100644 index 000000000000..d7a001c28d8e --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractImmediateHandlerSpringWebFluxServerTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; + +/** + * Tests the case where "controller" span is created within the route handler method scope, and the + * + *

{@code Mono} from a handler is already a fully constructed response with no + * deferred actions. For exception endpoint, the exception is thrown within route handler method + * scope. + */ +public abstract class AbstractImmediateHandlerSpringWebFluxServerTest + extends AbstractHandlerSpringWebFluxServerTest { + + @Test + void nestedPath() { + assumeTrue(Boolean.getBoolean("testLatestDeps")); + + String method = "GET"; + AggregatedHttpRequest request = request(NESTED_PATH, method); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + assertThat(response.status().code()).isEqualTo(NESTED_PATH.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(NESTED_PATH.getBody()); + assertResponseHasCustomizedHeaders(response, NESTED_PATH, null); + + assertTheTraces(1, null, null, null, method, NESTED_PATH); + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebFluxServerTest.java similarity index 93% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebFluxServerTest.java index 620737d41ce3..038714b3c793 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +package io.opentelemetry.instrumentation.spring.webflux.server; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; @@ -19,10 +19,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; -public abstract class SpringWebFluxServerTest +public abstract class AbstractSpringWebFluxServerTest extends AbstractHttpServerTest { - protected static final ServerEndpoint NESTED_PATH = + public static final ServerEndpoint NESTED_PATH = new ServerEndpoint("NESTED_PATH", "nestedPath/hello/world", 200, "nested path"); protected abstract Class getApplicationClass(); diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebfluxTest.java new file mode 100644 index 000000000000..a9c0444cff14 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/AbstractSpringWebfluxTest.java @@ -0,0 +1,763 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.server; + +import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionAssertions; +import static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionPrefixAssertions; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; +import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.EventDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Value; +import server.EchoHandlerFunction; +import server.FooModel; +import server.SpringWebFluxTestApplication; +import server.TestController; + +@SuppressWarnings("deprecation") // using deprecated semconv +public abstract class AbstractSpringWebfluxTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final String INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX = + SpringWebFluxTestApplication.class.getName() + "$"; + private static final String SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX = + SpringWebFluxTestApplication.class.getSimpleName() + "$"; + + // can't use @LocalServerPort annotation since it moved packages between Spring Boot 2 and 3 + @Value("${local.server.port}") + private int port; + + private WebClient client; + + @BeforeEach + void beforeEach() { + client = WebClient.builder("h1c://localhost:" + port).followRedirects().build(); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideParameters") + void basicGetTest(Parameter parameter) { + AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + parameter.urlPathWithVariables) + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), + span -> { + if (parameter.annotatedMethod == null) { + // Functional API + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + } else { + // Annotation API + span.hasName( + TestController.class.getSimpleName() + "." + parameter.annotatedMethod); + } + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); + })); + } + + private static List assertCodeFunction(Parameter parameter) { + String expectedFunctionName = + parameter.annotatedMethod == null ? "handle" : parameter.annotatedMethod; + String expectedPrefix = + parameter.annotatedMethod == null + ? INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX + : TestController.class.getName(); + + return codeFunctionPrefixAssertions(expectedPrefix, expectedFunctionName); + } + + private static Stream provideParameters() { + return Stream.of( + Arguments.of( + named( + "functional API without parameters", + new Parameter( + "/greet", + "/greet", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), + Arguments.of( + named( + "functional API with one parameter", + new Parameter( + "/greet/WORLD", + "/greet/{name}", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " WORLD"))), + Arguments.of( + named( + "functional API with two parameters", + new Parameter( + "/greet/World/Test1", + "/greet/{name}/{word}", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + + " World Test1"))), + Arguments.of( + named( + "functional API delayed response", + new Parameter( + "/greet-delayed", + "/greet-delayed", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), + Arguments.of( + named( + "annotation API without parameters", + new Parameter( + "/foo", "/foo", "getFooModel", new FooModel(0L, "DEFAULT").toString()))), + Arguments.of( + named( + "annotation API with one parameter", + new Parameter( + "/foo/1", "/foo/{id}", "getFooModel", new FooModel(1L, "pass").toString()))), + Arguments.of( + named( + "annotation API with two parameters", + new Parameter( + "/foo/2/world", + "/foo/{id}/{name}", + "getFooModel", + new FooModel(2L, "world").toString()))), + Arguments.of( + named( + "annotation API delayed response", + new Parameter( + "/foo-delayed", + "/foo-delayed", + "getFooDelayed", + new FooModel(3L, "delayed").toString()))), + Arguments.of( + named( + "annotation API without parameters no mono", + new Parameter( + "/foo-no-mono", + "/foo-no-mono", + "getFooModelNoMono", + new FooModel(0L, "DEFAULT").toString())))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideAsyncParameters") + void getAsyncResponseTest(Parameter parameter) { + AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + parameter.urlPathWithVariables) + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), + span -> { + if (parameter.annotatedMethod == null) { + // Functional API + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + } else { + // Annotation API + span.hasName( + TestController.class.getSimpleName() + "." + parameter.annotatedMethod); + } + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); + }, + span -> + span.hasName("tracedMethod") + .hasParent(trace.getSpan(1)) + .hasTotalAttributeCount(0))); + } + + private static Stream provideAsyncParameters() { + return Stream.of( + Arguments.of( + named( + "functional API traced method from mono", + new Parameter( + "/greet-mono-from-callable/4", + "/greet-mono-from-callable/{id}", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 4"))), + Arguments.of( + named( + "functional API traced method with delay", + new Parameter( + "/greet-delayed-mono/6", + "/greet-delayed-mono/{id}", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 6"))), + Arguments.of( + named( + "annotation API traced method from mono", + new Parameter( + "/foo-mono-from-callable/7", + "/foo-mono-from-callable/{id}", + "getMonoFromCallable", + new FooModel(7L, "tracedMethod").toString()))), + Arguments.of( + named( + "annotation API traced method with delay", + new Parameter( + "/foo-delayed-mono/9", + "/foo-delayed-mono/{id}", + "getFooDelayedMono", + new FooModel(9L, "tracedMethod").toString())))); + } + + /* + This test differs from the previous in one important aspect. + The test above calls endpoints which does not create any spans during their invocation. + They merely assemble reactive pipeline where some steps create spans. + Thus all those spans are created when WebFlux span created by DispatcherHandlerInstrumentation + has already finished. Therefore, they have `SERVER` span as their parent. + + This test below calls endpoints which do create spans right inside endpoint handler. + Therefore, in theory, those spans should have INTERNAL span created by DispatcherHandlerInstrumentation + as their parent. But there is a difference how Spring WebFlux handles functional endpoints + (created in server.SpringWebFluxTestApplication.greetRouterFunction) and annotated endpoints + (created in server.TestController). + In the former case org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle + calls handler function directly. Thus "tracedMethod" span below has INTERNAL handler span as its parent. + In the latter case org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.handle + merely wraps handler call into Mono and thus actual invocation of handler function happens later, + when INTERNAL handler span has already finished. Thus, "tracedMethod" has SERVER Netty span as its parent. + */ + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideAsyncHandlerFuncParameters") + void createSpanDuringHandlerFunctionTest(Parameter parameter) { + AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(parameter.expectedResponseBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + parameter.urlPathWithVariables) + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), + span -> { + if (parameter.annotatedMethod == null) { + // Functional API + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + } else { + // Annotation API + span.hasName( + TestController.class.getSimpleName() + "." + parameter.annotatedMethod); + } + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); + }, + span -> + span.hasName("tracedMethod") + .hasParent(trace.getSpan(1)) + .hasTotalAttributeCount(0))); + } + + private static Stream provideAsyncHandlerFuncParameters() { + return Stream.of( + Arguments.of( + named( + "functional API traced method", + new Parameter( + "/greet-traced-method/5", + "/greet-traced-method/{id}", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 5"))), + Arguments.of( + named( + "annotation API traced method", + new Parameter( + "/foo-traced-method/8", + "/foo-traced-method/{id}", + "getTracedMethod", + new FooModel(8L, "tracedMethod").toString())))); + } + + @Test + void get404Test() { + AggregatedHttpResponse response = client.get("/notfoundgreet").aggregate().join(); + + assertThat(response.status().code()).isEqualTo(404); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /**") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, "/notfoundgreet"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 404), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/**")), + span -> + span.hasName("ResourceWebHandler.handle") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly(AbstractSpringWebfluxTest::resource404Exception) + .hasAttributesSatisfyingExactly( + codeFunctionAssertions( + "org.springframework.web.reactive.resource.ResourceWebHandler", + "handle")))); + } + + private static void resource404Exception(EventDataAssert event) { + if (Boolean.getBoolean("testLatestDeps")) { + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + "org.springframework.web.reactive.resource.NoResourceFoundException"), + satisfies(EXCEPTION_MESSAGE, val -> val.isInstanceOf(String.class)), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); + } else { + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + satisfies( + EXCEPTION_TYPE, + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.server.ResponseStatusException"), + // Changed in spring 7+ + v -> + assertThat(v) + .isEqualTo( + "org.springframework.web.reactive.resource.NoResourceFoundException"))), + satisfies(EXCEPTION_MESSAGE, val -> val.contains("404")), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); + } + } + + @Test + void basicPostTest() { + String echoString = "TEST"; + AggregatedHttpResponse response = client.post("/echo", echoString).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(202); + assertThat(response.contentUtf8()).isEqualTo(echoString); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST /echo") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, "/echo"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 202), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/echo")), + span -> + span.hasName(EchoHandlerFunction.class.getSimpleName() + ".handle") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + codeFunctionAssertions(EchoHandlerFunction.class, "handle")), + span -> + span.hasName("echo").hasParent(trace.getSpan(1)).hasTotalAttributeCount(0))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideBadEndpointParameters") + void getToBadEndpointTest(Parameter parameter) { + AggregatedHttpResponse response = client.get(parameter.urlPath).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(500); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + parameter.urlPathWithVariables) + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 500), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), + equalTo(ERROR_TYPE, "500")), + span -> { + if (parameter.annotatedMethod == null) { + // Functional API + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + } else { + // Annotation API + span.hasName( + TestController.class.getSimpleName() + "." + parameter.annotatedMethod); + } + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), + equalTo(EXCEPTION_MESSAGE, "bad things happen"), + satisfies( + EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); + })); + } + + private static Stream provideBadEndpointParameters() { + return Stream.of( + Arguments.of( + named( + "functional API fail fast", + new Parameter("/greet-failfast/1", "/greet-failfast/{id}", null, null))), + Arguments.of( + named( + "functional API fail Mono", + new Parameter("/greet-failmono/1", "/greet-failmono/{id}", null, null))), + Arguments.of( + named( + "annotation API fail fast", + new Parameter("/foo-failfast/1", "/foo-failfast/{id}", "getFooFailFast", null))), + Arguments.of( + named( + "annotation API fail Mono", + new Parameter("/foo-failmono/1", "/foo-failmono/{id}", "getFooFailMono", null)))); + } + + @Test + void redirectTest() { + AggregatedHttpResponse response = client.get("/double-greet-redirect").aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + testing.waitAndAssertTraces( + trace -> + // TODO: why order of spans is different in these traces? + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /double-greet-redirect") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, "/double-greet-redirect"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 307), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/double-greet-redirect")), + span -> + span.hasName("RedirectComponent$$Lambda.handle") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + codeFunctionPrefixAssertions( + "server.RedirectComponent$$Lambda", "handle"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /double-greet") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, "/double-greet"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/double-greet")), + span -> { + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + codeFunctionPrefixAssertions( + INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX, "handle")); + })); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideMultipleDelayingRouteParameters") + void multipleGetsToDelayingRoute(Parameter parameter) { + int requestsCount = 50; + + List responses = + IntStream.rangeClosed(0, requestsCount - 1) + .mapToObj(n -> client.get(parameter.urlPath).aggregate().join()) + .collect(Collectors.toList()); + + assertThat(responses) + .extracting(AggregatedHttpResponse::status) + .extracting(HttpStatus::code) + .containsOnly(200); + assertThat(responses) + .extracting(AggregatedHttpResponse::contentUtf8) + .containsOnly(parameter.expectedResponseBody); + + Consumer traceAssertion = + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + parameter.urlPathWithVariables) + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), + span -> { + if (parameter.annotatedMethod == null) { + // Functional API + assertThat(trace.getSpan(1).getName()) + .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); + } else { + // Annotation API + span.hasName( + TestController.class.getSimpleName() + "." + parameter.annotatedMethod); + } + span.hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(assertCodeFunction(parameter)); + }); + + testing.waitAndAssertTraces(Collections.nCopies(requestsCount, traceAssertion)); + } + + private static Stream provideMultipleDelayingRouteParameters() { + return Stream.of( + Arguments.of( + named( + "functional API delayed response", + new Parameter( + "/greet-delayed", + "/greet-delayed", + null, + SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE))), + Arguments.of( + named( + "annotation API delayed response", + new Parameter( + "/foo-delayed", + "/foo-delayed", + "getFooDelayed", + new FooModel(3L, "delayed").toString())))); + } + + @Test + void cancelRequestTest() throws InterruptedException { + // fails with SingleThreadedSpringWebfluxTest + Assumptions.assumeTrue(this.getClass().getSimpleName().startsWith("SpringWebfluxTest")); + + WebClient client = + WebClient.builder("h1c://localhost:" + port) + .responseTimeout(Duration.ofSeconds(1)) + .followRedirects() + .build(); + try { + client.get("/slow").aggregate().get(); + } catch (ExecutionException ignore) { + // ignore + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /slow") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(URL_PATH, "/slow"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/slow"), + equalTo(ERROR_TYPE, "_OTHER")), + span -> + span.hasName("SpringWebFluxTestApplication$$Lambda.handle") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + codeFunctionPrefixAssertions( + "server.SpringWebFluxTestApplication$$Lambda", "handle")))); + + SpringWebFluxTestApplication.resumeSlowRequest(); + } + + private static class Parameter { + final String urlPath; + final String urlPathWithVariables; + final String annotatedMethod; + final String expectedResponseBody; + + Parameter( + String urlPath, + String urlPathWithVariables, + String annotatedMethod, + String expectedResponseBody) { + this.urlPath = urlPath; + this.urlPathWithVariables = urlPathWithVariables; + this.annotatedMethod = annotatedMethod; + this.expectedResponseBody = expectedResponseBody; + } + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/ServerTestController.java similarity index 72% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/ServerTestController.java index 432f23eb961a..320cde7873dd 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/ServerTestController.java @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +package io.opentelemetry.instrumentation.spring.webflux.server; -import static io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base.SpringWebFluxServerTest.NESTED_PATH; +import static io.opentelemetry.instrumentation.spring.webflux.server.AbstractSpringWebFluxServerTest.NESTED_PATH; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.lang.reflect.Method; import java.net.URI; import java.util.function.Supplier; import org.springframework.http.HttpStatus; @@ -17,7 +18,29 @@ import org.springframework.web.bind.annotation.PathVariable; import reactor.core.publisher.Mono; +@SuppressWarnings("IdentifierName") // method names are snake_case to match endpoints public abstract class ServerTestController { + + // Spring 5.x uses setStatusCode(HttpStatus), Spring 6+ uses setStatusCode(HttpStatusCode) + private static final Method setStatusCodeMethod; + + static { + Method method; + try { + // Try Spring 6+ signature first (HttpStatusCode interface) + Class httpStatusCodeClass = Class.forName("org.springframework.http.HttpStatusCode"); + method = ServerHttpResponse.class.getMethod("setStatusCode", httpStatusCodeClass); + } catch (ClassNotFoundException | NoSuchMethodException e) { + // Fall back to Spring 5.x signature (HttpStatus enum) + try { + method = ServerHttpResponse.class.getMethod("setStatusCode", HttpStatus.class); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } + setStatusCodeMethod = method; + } + @GetMapping("/success") public Mono success(ServerHttpResponse response) { ServerEndpoint endpoint = ServerEndpoint.SUCCESS; @@ -132,7 +155,12 @@ public Mono nested_path(ServerHttpRequest request, ServerHttpResponse re protected abstract Mono wrapControllerMethod(ServerEndpoint endpoint, Supplier handler); - private static void setStatus(ServerHttpResponse response, ServerEndpoint endpoint) { - response.setStatusCode(HttpStatus.resolve(endpoint.getStatus())); + protected void setStatus(ServerHttpResponse response, ServerEndpoint endpoint) { + HttpStatus status = HttpStatus.resolve(endpoint.getStatus()); + try { + setStatusCodeMethod.invoke(response, status); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to set status code", e); + } } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/EchoHandler.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/EchoHandler.java similarity index 100% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/EchoHandler.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/EchoHandler.java diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/EchoHandlerFunction.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/EchoHandlerFunction.java similarity index 100% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/EchoHandlerFunction.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/EchoHandlerFunction.java diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/FooModel.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/FooModel.java similarity index 100% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/FooModel.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/FooModel.java diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/RedirectComponent.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/RedirectComponent.java similarity index 100% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/RedirectComponent.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/RedirectComponent.java diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/SpringWebFluxTestApplication.java similarity index 84% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/SpringWebFluxTestApplication.java index 0151501226ed..94a36c08a6b2 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/SpringWebFluxTestApplication.java @@ -81,7 +81,9 @@ RouterFunction greetRouterFunction(GreetingHandler greetingHandl throw new IllegalStateException(e); } return Mono.delay(Duration.ofMillis(100)) - .then(ServerResponse.ok().body(BodyInserters.fromObject("ok"))); + .then( + ServerResponse.ok() + .body(BodyInserters.fromPublisher(Mono.just("ok"), String.class))); }); } @@ -96,31 +98,37 @@ public static class GreetingHandler { Mono defaultGreet() { return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) - .body(BodyInserters.fromObject(DEFAULT_RESPONSE)); + .body(BodyInserters.fromPublisher(Mono.just(DEFAULT_RESPONSE), String.class)); } Mono doubleGreet() { return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) - .body(BodyInserters.fromObject(DEFAULT_RESPONSE + DEFAULT_RESPONSE)); + .body( + BodyInserters.fromPublisher( + Mono.just(DEFAULT_RESPONSE + DEFAULT_RESPONSE), String.class)); } Mono customGreet(ServerRequest request) { return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) - .body(BodyInserters.fromObject(DEFAULT_RESPONSE + " " + request.pathVariable("name"))); + .body( + BodyInserters.fromPublisher( + Mono.just(DEFAULT_RESPONSE + " " + request.pathVariable("name")), String.class)); } Mono customGreetWithWord(ServerRequest request) { return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) .body( - BodyInserters.fromObject( - DEFAULT_RESPONSE - + " " - + request.pathVariable("name") - + " " - + request.pathVariable("word"))); + BodyInserters.fromPublisher( + Mono.just( + DEFAULT_RESPONSE + + " " + + request.pathVariable("name") + + " " + + request.pathVariable("word")), + String.class)); } Mono intResponse(Mono mono) { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/TestController.java similarity index 100% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/server/TestController.java 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 3ef71a9ab6d2..1244902a486e 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 @@ -108,6 +108,7 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass("org.springframework.boot.servlet.filter.") .allowClass("org.springframework.boot.web.server.servlet.context.") .allowClass("org.springframework.boot.web.embedded.netty.GracefulShutdown$$Lambda") + .allowClass("org.springframework.boot.reactor.netty.GracefulShutdown$$Lambda") .allowClass("org.springframework.boot.web.embedded.tomcat.GracefulShutdown$$Lambda") .allowClass("org.springframework.boot.tomcat.GracefulShutdown$$Lambda") .allowClass("org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory$$Lambda") @@ -126,6 +127,7 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass( "org.springframework.boot.autoconfigure.web.WebProperties$Resources$Cache$Cachecontrol$$Lambda") .allowClass("org.springframework.boot.web.embedded.netty.NettyWebServer$") + .allowClass("org.springframework.boot.reactor.netty.NettyWebServer$") .allowClass("org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext$$Lambda") .allowClass("org.springframework.boot.tomcat.TomcatEmbeddedContext$$Lambda") .allowClass("org.springframework.boot.tomcat.TomcatWebServer$") diff --git a/settings.gradle.kts b/settings.gradle.kts index c175394c2dd4..80ea086b6811 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -646,6 +646,7 @@ include(":instrumentation:spring:spring-web:spring-web-3.1:library") include(":instrumentation:spring:spring-web:spring-web-6.0:javaagent") include(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent") include(":instrumentation:spring:spring-webflux:spring-webflux-5.0:testing") +include(":instrumentation:spring:spring-webflux:spring-webflux-5.0:testing-webflux7") include(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library") include(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing") include(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing-webflux7")