diff --git a/.fossa.yml b/.fossa.yml index 7422e7ce8718..97aee67024cb 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -1138,6 +1138,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-webmvc-4.3:javaagent' - type: gradle path: ./ target: ':instrumentation:spring:spring-data:spring-data-1.8:javaagent' diff --git a/.github/scripts/check-javaagent-suppression-keys.sh b/.github/scripts/check-javaagent-suppression-keys.sh index 23528e194983..5dce5f2dbdfa 100755 --- a/.github/scripts/check-javaagent-suppression-keys.sh +++ b/.github/scripts/check-javaagent-suppression-keys.sh @@ -33,6 +33,10 @@ for file in $(find instrumentation -name "*Module.java"); do # TODO module is missing a base version continue fi + if [[ "$simple_module_name" == spring-cloud-gateway-webmvc ]]; then + # webmvc variant uses spring-cloud-gateway as base name + simple_module_name="spring-cloud-gateway" + fi if [ "$module_name" == "$simple_module_name" ]; then expected="super\(\n? *\"$simple_module_name\"" diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts index 8e0e217bfbc1..cd64c197c41f 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts @@ -7,7 +7,16 @@ muzzle { pass { group.set("org.springframework.cloud") module.set("spring-cloud-starter-gateway") - versions.set("[2.0.0.RELEASE,]") + versions.set("[2.0.0.RELEASE,)") + assertInverse.set(true) + } + + // Spring Cloud Gateway 4.3.0+ split into separate artifacts + // see spring-cloud-starter-gateway-server-webmvc-4.3 for mvc + pass { + group.set("org.springframework.cloud") + module.set("spring-cloud-starter-gateway-server-webflux") + versions.set("[4.3.0,]") assertInverse.set(true) } } @@ -15,6 +24,8 @@ muzzle { dependencies { library("org.springframework.cloud:spring-cloud-starter-gateway:2.0.0.RELEASE") + implementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common: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")) diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java index 567f54f0c332..695bf2f7be81 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; -import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; @@ -23,7 +23,7 @@ public GatewayInstrumentationModule() { @Override public List typeInstrumentations() { - return asList(new HandlerAdapterInstrumentation()); + return singletonList(new HandlerAdapterInstrumentation()); } @Override diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java index f93f7f4362f2..ab557ca9ddda 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java @@ -5,59 +5,28 @@ package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_FILTER_SIZE_ATTRIBUTE; +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ID_ATTRIBUTE; +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ORDER_ATTRIBUTE; +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_URI_ATTRIBUTE; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; -import java.util.regex.Pattern; +import io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper; import javax.annotation.Nullable; import org.springframework.cloud.gateway.route.Route; import org.springframework.web.server.ServerWebExchange; public final class ServerWebExchangeHelper { - /** Route ID attribute key. */ - private static final AttributeKey ROUTE_ID_ATTRIBUTE = - AttributeKey.stringKey("spring-cloud-gateway.route.id"); - - /** Route URI attribute key. */ - private static final AttributeKey ROUTE_URI_ATTRIBUTE = - AttributeKey.stringKey("spring-cloud-gateway.route.uri"); - - /** Route order attribute key. */ - private static final AttributeKey ROUTE_ORDER_ATTRIBUTE = - AttributeKey.longKey("spring-cloud-gateway.route.order"); - - /** Route filter size attribute key. */ - private static final AttributeKey ROUTE_FILTER_SIZE_ATTRIBUTE = - AttributeKey.longKey("spring-cloud-gateway.route.filter.size"); - - private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; - - static { - CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - AgentInstrumentationConfig.get() - .getBoolean( - "otel.instrumentation.spring-cloud-gateway.experimental-span-attributes", false); - } - - /* Regex for UUID */ - private static final Pattern UUID_REGEX = - Pattern.compile( - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); - - private static final String INVALID_RANDOM_ROUTE_ID = - "org.springframework.util.AlternativeJdkIdGenerator@"; - private ServerWebExchangeHelper() {} public static void extractAttributes(ServerWebExchange exchange, Context context) { // Record route info Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); - if (route != null && CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + if (route != null && GatewayRouteHelper.shouldCaptureExperimentalSpanAttributes()) { Span serverSpan = LocalRootSpan.fromContextOrNull(context); if (serverSpan == null) { return; @@ -76,30 +45,8 @@ public static String extractServerRoute(@Nullable ServerWebExchange exchange) { } Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); if (route != null) { - return convergeRouteId(route); + return GatewayRouteHelper.convergeRouteId(route.getId()); } return null; } - - /** - * To avoid high cardinality, we ignore random UUID generated by Spring Cloud Gateway. Spring - * Cloud Gateway generate invalid random routeID, and it is fixed until 3.1.x - * - * @see - */ - @Nullable - private static String convergeRouteId(Route route) { - String routeId = route.getId(); - if (routeId == null || routeId.isEmpty()) { - return null; - } - if (UUID_REGEX.matcher(routeId).matches()) { - return null; - } - if (routeId.startsWith(INVALID_RANDOM_ROUTE_ID)) { - return null; - } - return routeId; - } } diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java index d3a907ec959b..26f49d36ab37 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java @@ -5,12 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; -import org.junit.jupiter.api.Test; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import java.util.List; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -18,60 +15,26 @@ @ExtendWith(SpringExtension.class) @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { - GatewayTestApplication.class, - GatewayRouteMappingTest.ForceNettyAutoConfiguration.class - }) + classes = {GatewayTestApplication.class}) class GatewayRouteMappingTest extends AbstractRouteMappingTest { - @Test - void gatewayRouteMappingTest() { - String requestBody = "gateway"; - AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join(); - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(requestBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST path_route") - .hasKind(SpanKind.SERVER) - .hasAttributesSatisfying( - buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1)), - span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + @Override + protected String getSpanName() { + return "POST path_route"; + } + + @Override + protected List getExpectedAttributes() { + return buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1); } - @Test - void gatewayRandomUuidRouteMappingTest() { - String requestBody = "gateway"; - AggregatedHttpResponse response = client.post("/uuid/echo", requestBody).aggregate().join(); - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(requestBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST") - .hasKind(SpanKind.SERVER) - .hasAttributesSatisfying(buildAttributeAssertions("h1c://mock.uuid", 0, 1)), - span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + @Override + protected List getRandomUuidExpectedAttributes() { + return buildAttributeAssertions("h1c://mock.uuid", 0, 1); } - @Test - void gatewayFakeUuidRouteMappingTest() { - String requestBody = "gateway"; - String routeId = "ffffffff-ffff-ffff-ffff-ffff"; - AggregatedHttpResponse response = client.post("/fake/echo", requestBody).aggregate().join(); - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(requestBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST " + routeId) - .hasKind(SpanKind.SERVER) - .hasAttributesSatisfying( - buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1)), - span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + @Override + protected List getFakeUuidExpectedAttributes(String routeId) { + return buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1); } } diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts index 37a67d795322..24b5812b74bb 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts @@ -14,10 +14,11 @@ dependencies { testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE") testLibrary("org.springframework.boot:spring-boot-starter-test:2.2.0.RELEASE") - // tests don't work with spring boot 4 yet - latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // documented limitation + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // see spring-cloud-gateway-4.3* module } +val latestDepTest = findProperty("testLatestDeps") as Boolean + tasks.withType().configureEach { jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") @@ -27,11 +28,9 @@ tasks.withType().configureEach { jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + systemProperty("testLatestDeps", latestDepTest) } -val latestDepTest = findProperty("testLatestDeps") as Boolean - if (latestDepTest) { // spring 6 requires java 17 otelJava { diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java index 9f3279be7f1d..c79bf41698bc 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java @@ -5,37 +5,10 @@ package io.opentelemetry.instrumentation.spring.gateway.v2_2; -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { - Gateway22TestApplication.class, - Gateway22RouteMappingTest.ForceNettyAutoConfiguration.class - }) -class Gateway22RouteMappingTest extends AbstractRouteMappingTest { - - @Test - void gatewayRouteMappingTest() { - String requestBody = "gateway"; - AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join(); - assertThat(response.status().code()).isEqualTo(200); - assertThat(response.contentUtf8()).isEqualTo(requestBody); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST") - .hasKind(SpanKind.SERVER) - .hasAttributesSatisfying( - // Global filter is not route filter, so filter size should be 0. - buildAttributeAssertions("h1c://mock.response", 2023, 0)), - span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); - } -} + classes = {Gateway22TestApplication.class}) +class Gateway22RouteMappingTest extends AbstractRouteMappingTest {} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java index b8fd00860440..f47685d374a4 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java @@ -5,14 +5,8 @@ package io.opentelemetry.instrumentation.spring.gateway.v2_2; +import io.opentelemetry.instrumentation.spring.gateway.common.GatewayTestApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.gateway.filter.GlobalFilter; -import org.springframework.context.annotation.Bean; @SpringBootApplication -public class Gateway22TestApplication { - @Bean - public GlobalFilter echoFilter() { - return (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody()); - } -} +public class Gateway22TestApplication extends GatewayTestApplication {} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml index 495bde2f5c16..9b5287472180 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml @@ -6,3 +6,14 @@ spring: predicates: - Path=/gateway/echo order: 2023 + # Route without ID - will get random UUID (should be filtered out) + - uri: h1c://mock.uuid + predicates: + - Path=/uuid/echo + order: 0 + # Route with fake UUID ID (should NOT be filtered out) + - id: ffffffff-ffff-ffff-ffff-ffff + uri: h1c://mock.fake + predicates: + - Path=/fake/echo + order: 0 diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/build.gradle.kts new file mode 100644 index 000000000000..0b6bd5f67942 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("otel.javaagent-instrumentation") +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/common/GatewayRouteHelper.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/common/GatewayRouteHelper.java new file mode 100644 index 000000000000..89d8b72e917e --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/common/GatewayRouteHelper.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.common; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * Shared helper class for Spring Cloud Gateway instrumentation across different versions (WebFlux + * and WebMVC). + */ +public final class GatewayRouteHelper { + + /** Route ID attribute key. */ + public static final AttributeKey ROUTE_ID_ATTRIBUTE = + AttributeKey.stringKey("spring-cloud-gateway.route.id"); + + /** Route URI attribute key. */ + public static final AttributeKey ROUTE_URI_ATTRIBUTE = + AttributeKey.stringKey("spring-cloud-gateway.route.uri"); + + /** Route order attribute key. */ + public static final AttributeKey ROUTE_ORDER_ATTRIBUTE = + AttributeKey.longKey("spring-cloud-gateway.route.order"); + + /** Route filter size attribute key. */ + public static final AttributeKey ROUTE_FILTER_SIZE_ATTRIBUTE = + AttributeKey.longKey("spring-cloud-gateway.route.filter.size"); + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + + static { + CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.spring-cloud-gateway.experimental-span-attributes", false); + } + + /* Regex for UUID */ + private static final Pattern UUID_REGEX = + Pattern.compile( + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + private static final String INVALID_RANDOM_ROUTE_ID = + "org.springframework.util.AlternativeJdkIdGenerator@"; + + private GatewayRouteHelper() {} + + /** Returns whether experimental span attributes should be captured. */ + public static boolean shouldCaptureExperimentalSpanAttributes() { + return CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + } + + /** + * To avoid high cardinality, we ignore random UUID generated by Spring Cloud Gateway. Spring + * Cloud Gateway generates invalid random routeID, and it is fixed until 3.1.x + * + * @see Spring + * Cloud Gateway commit + */ + @Nullable + public static String convergeRouteId(@Nullable String routeId) { + if (routeId == null || routeId.isEmpty()) { + return null; + } + if (UUID_REGEX.matcher(routeId).matches()) { + return null; + } + if (routeId.startsWith(INVALID_RANDOM_ROUTE_ID)) { + return null; + } + return routeId; + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts index 7e8f0a0dd6d5..95af1a7be3bb 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts @@ -7,4 +7,5 @@ dependencies { implementation("io.opentelemetry.javaagent:opentelemetry-testing-common") compileOnly("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") + compileOnly("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE") } diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java index d9b6c94fa8da..21ecc541e2dd 100644 --- a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java @@ -5,33 +5,27 @@ package io.opentelemetry.instrumentation.spring.gateway.common; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; -import io.opentelemetry.api.common.AttributeKey; +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.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; -import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; public abstract class AbstractRouteMappingTest { - @TestConfiguration - public static class ForceNettyAutoConfiguration { - @Bean - NettyReactiveWebServerFactory nettyFactory() { - return new NettyReactiveWebServerFactory(); - } - } - @RegisterExtension protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @@ -42,21 +36,98 @@ NettyReactiveWebServerFactory nettyFactory() { protected static final String WEBFLUX_SPAN_NAME = "FilteringWebHandler.handle"; + protected String getSpanName() { + return "POST"; + } + + protected String getInternalSpanName() { + return WEBFLUX_SPAN_NAME; + } + + protected List getExpectedAttributes() { + // Global filter is not route filter, so filter size should be 0. + return buildAttributeAssertions("h1c://mock.response", 2023, 0); + } + @BeforeEach void beforeEach() { client = WebClient.builder("h1c://localhost:" + port).followRedirects().build(); } + @Test + protected void testGatewayRouteMapping() { + String requestBody = "gateway"; + AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getSpanName()) + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying(getExpectedAttributes()), + span -> span.hasName(getInternalSpanName()).hasKind(SpanKind.INTERNAL))); + } + + @Test + protected void testRandomUuidRouteFiltering() { + String requestBody = "gateway"; + AggregatedHttpResponse response = client.post("/uuid/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getRandomUuidSpanName()) + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying(getRandomUuidExpectedAttributes()), + span -> span.hasName(getInternalSpanName()).hasKind(SpanKind.INTERNAL))); + } + + @Test + protected void testFakeUuidRouteNotFiltered() { + String requestBody = "gateway"; + String routeId = "ffffffff-ffff-ffff-ffff-ffff"; + AggregatedHttpResponse response = client.post("/fake/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getFakeUuidSpanName(routeId)) + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying(getFakeUuidExpectedAttributes(routeId)), + span -> span.hasName(getInternalSpanName()).hasKind(SpanKind.INTERNAL))); + } + + protected String getRandomUuidSpanName() { + return "POST"; + } + + protected List getRandomUuidExpectedAttributes() { + return buildAttributeAssertions("h1c://mock.uuid", 0, 0); + } + + protected String getFakeUuidSpanName(String routeId) { + return "POST " + routeId; + } + + protected List getFakeUuidExpectedAttributes(String routeId) { + return buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 0); + } + protected List buildAttributeAssertions( @Nullable String routeId, String uri, int order, int filterSize) { List assertions = new ArrayList<>(); if (!StringUtils.isEmpty(routeId)) { - assertions.add(equalTo(AttributeKey.stringKey("spring-cloud-gateway.route.id"), routeId)); + assertions.add(equalTo(stringKey("spring-cloud-gateway.route.id"), routeId)); } - assertions.add(equalTo(AttributeKey.stringKey("spring-cloud-gateway.route.uri"), uri)); - assertions.add(equalTo(AttributeKey.longKey("spring-cloud-gateway.route.order"), order)); - assertions.add( - equalTo(AttributeKey.longKey("spring-cloud-gateway.route.filter.size"), filterSize)); + assertions.add(equalTo(stringKey("spring-cloud-gateway.route.uri"), uri)); + assertions.add(equalTo(longKey("spring-cloud-gateway.route.order"), order)); + assertions.add(equalTo(longKey("spring-cloud-gateway.route.filter.size"), filterSize)); return assertions; } diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/GatewayTestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/GatewayTestApplication.java new file mode 100644 index 000000000000..9a6d35a59c62 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/GatewayTestApplication.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.common; + +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; + +public abstract class GatewayTestApplication { + @Bean + public GlobalFilter echoFilter() { + return (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody()); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/build.gradle.kts new file mode 100644 index 000000000000..51d6efbf8fd1 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testInstrumentation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.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")) + testInstrumentation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent")) + + testImplementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing")) + + // classes in test setup require 5.0+ + testLibrary("org.springframework.cloud:spring-cloud-starter-gateway-server-webflux:5.0.0") + testLibrary("org.springframework.boot:spring-boot-starter-test:4.0.0") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} + +// spring 7 requires java 17 +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43RouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43RouteMappingTest.java new file mode 100644 index 000000000000..c0038646aa44 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43RouteMappingTest.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.v4_3; + +import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = {Gateway43TestApplication.class}) +class Gateway43RouteMappingTest extends AbstractRouteMappingTest {} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43TestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43TestApplication.java new file mode 100644 index 000000000000..b8a246b80acf --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v4_3/Gateway43TestApplication.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.v4_3; + +import io.opentelemetry.instrumentation.spring.gateway.common.GatewayTestApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Gateway43TestApplication extends GatewayTestApplication {} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/resources/application.yml b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/resources/application.yml new file mode 100644 index 000000000000..df348f4b5bc7 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webflux-4.3/testing/src/test/resources/application.yml @@ -0,0 +1,21 @@ +spring: + cloud: + gateway: + server: + webflux: + routes: + - uri: h1c://mock.response + predicates: + - Path=/gateway/echo + order: 2023 + # Route without ID - will get random UUID (should be filtered out) + - uri: h1c://mock.uuid + predicates: + - Path=/uuid/echo + order: 0 + # Route with fake UUID ID (should NOT be filtered out) + - id: ffffffff-ffff-ffff-ffff-ffff + uri: h1c://mock.fake + predicates: + - Path=/fake/echo + order: 0 \ No newline at end of file diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/build.gradle.kts new file mode 100644 index 000000000000..368787c4e4de --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("otel.javaagent-instrumentation") + id("otel.nullaway-conventions") +} + +muzzle { + pass { + group.set("org.springframework.cloud") + module.set("spring-cloud-starter-gateway-server-webmvc") + versions.set("[4.3.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc:4.3.0") + + implementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent")) + + testInstrumentation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:javaagent")) + testInstrumentation(project(":instrumentation:tomcat:tomcat-10.0:javaagent")) + + testImplementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing")) + + testLibrary("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc:5.0.0") + testLibrary("org.springframework.boot:spring-boot-starter-test:4.0.0") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} + +// spring 7 requires java 17 +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayDelegatingRouterFunctionInstrumentation.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayDelegatingRouterFunctionInstrumentation.java new file mode 100644 index 000000000000..f647e468eed9 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayDelegatingRouterFunctionInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.webmvc.v5_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.servlet.function.ServerRequest; + +public class GatewayDelegatingRouterFunctionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named( + "org.springframework.cloud.gateway.server.mvc.handler.GatewayDelegatingRouterFunction"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("route")) + .and(takesArgument(0, named("org.springframework.web.servlet.function.ServerRequest"))) + .and(takesArguments(1)), + this.getClass().getName() + "$RouteAdvice"); + } + + @SuppressWarnings("unused") + public static class RouteAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit( + @Advice.This Object thisObj, @Advice.Argument(0) ServerRequest request) { + Context context = Context.current(); + // Record gateway route info as attributes + // The HTTP route should remain the actual path pattern from Spring WebMVC + ServerRequestHelper.extractAttributes(thisObj, request, context); + } + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayWebMvcInstrumentationModule.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayWebMvcInstrumentationModule.java new file mode 100644 index 000000000000..7e47f1194b6f --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/GatewayWebMvcInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.webmvc.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class GatewayWebMvcInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public GatewayWebMvcInstrumentationModule() { + super("spring-cloud-gateway", "spring-cloud-gateway-webmvc-4.3"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "org.springframework.cloud.gateway.server.mvc.handler.GatewayDelegatingRouterFunction"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new GatewayDelegatingRouterFunctionInstrumentation()); + } + + @Override + public int order() { + // Later than Spring WebMVC + return 1; + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/ServerRequestHelper.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/ServerRequestHelper.java new file mode 100644 index 000000000000..93c2d766f593 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/ServerRequestHelper.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.webmvc.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ID_ATTRIBUTE; +import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_URI_ATTRIBUTE; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper; +import java.lang.reflect.Field; +import javax.annotation.Nullable; +import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; +import org.springframework.cloud.gateway.server.mvc.handler.GatewayDelegatingRouterFunction; +import org.springframework.web.servlet.function.ServerRequest; + +/** + * Helper class for extracting Spring Cloud Gateway Server WebMVC route information from + * ServerRequest and adding it to spans. + */ +public final class ServerRequestHelper { + @Nullable private static final Field routeIdField; + + static { + Field field = null; + try { + field = GatewayDelegatingRouterFunction.class.getDeclaredField("routeId"); + field.setAccessible(true); + } catch (Exception ignored) { + // Ignored + } + + routeIdField = field; + } + + public static void extractAttributes( + Object gatewayRouterFunction, ServerRequest request, Context context) { + if (routeIdField == null || !GatewayRouteHelper.shouldCaptureExperimentalSpanAttributes()) { + return; + } + + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + String routeId = null; + try { + routeId = (String) routeIdField.get(gatewayRouterFunction); + } catch (Exception ignored) { + // Silently ignore + } + + String convergedRouteId = GatewayRouteHelper.convergeRouteId(routeId); + if (convergedRouteId != null) { + serverSpan.setAttribute(ROUTE_ID_ATTRIBUTE, convergedRouteId); + } + + request + .attribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR) + .ifPresent(uri -> serverSpan.setAttribute(ROUTE_URI_ATTRIBUTE, uri.toString())); + } + + private ServerRequestHelper() {} +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcRouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcRouteMappingTest.java new file mode 100644 index 000000000000..54ddc7eb7d2e --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcRouteMappingTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.webmvc.v5_0; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = {Gateway43MvcTestApplication.class}) +class Gateway43MvcRouteMappingTest extends AbstractRouteMappingTest { + @Override + protected String getSpanName() { + return "POST /gateway/echo"; + } + + @Override + protected String getInternalSpanName() { + // WebMVC creates an internal span for the handler filter function + return "HandlerFilterFunction$$Lambda."; + } + + /** + * WebMVC variant only has access to route ID at the point of instrumentation. Route URI is set by + * filters that execute later and is not available when the route method is called. Order and + * filter size are not available in request attributes. + */ + @Override + protected List getExpectedAttributes() { + List assertions = new ArrayList<>(); + assertions.add(equalTo(stringKey("spring-cloud-gateway.route.id"), "test-route-id")); + return assertions; + } + + @Override + protected String getRandomUuidSpanName() { + // WebMVC uses HTTP route in span name, not gateway route ID + return "POST /uuid/echo"; + } + + @Override + protected List getRandomUuidExpectedAttributes() { + return new ArrayList<>(); + } + + @Override + protected String getFakeUuidSpanName(String routeId) { + // WebMVC uses HTTP route in span name, not gateway route ID + return "POST /fake/echo"; + } + + @Override + protected List getFakeUuidExpectedAttributes(String routeId) { + List assertions = new ArrayList<>(); + assertions.add(equalTo(stringKey("spring-cloud-gateway.route.id"), routeId)); + return assertions; + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcTestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcTestApplication.java new file mode 100644 index 000000000000..e13344f88299 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-webmvc-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/webmvc/v5_0/Gateway43MvcTestApplication.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.webmvc.v5_0; + +import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri; +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; + +import java.io.IOException; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +@SpringBootApplication +public class Gateway43MvcTestApplication { + @Bean + public RouterFunction gatewayRouterFunction() { + HandlerFunction echoHandler = + request -> { + try { + String body = request.body(String.class); + return ServerResponse.status(HttpStatus.OK) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(body); + } catch (IOException e) { + return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + }; + + RouterFunction mainRoute = + route("test-route-id") + .POST("/gateway/echo", echoHandler) + .before(uri("http://mock.response")) + .build(); + + RouterFunction uuidRoute = + route().POST("/uuid/echo", echoHandler).before(uri("http://mock.uuid")).build(); + + RouterFunction fakeUuidRoute = + route("ffffffff-ffff-ffff-ffff-ffff") + .POST("/fake/echo", echoHandler) + .before(uri("http://mock.fake")) + .build(); + + return mainRoute.and(uuidRoute).and(fakeUuidRoute); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 334680435c8c..ee3e77714abe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -615,6 +615,9 @@ include(":instrumentation:spring:spring-boot-resources:javaagent-unit-tests") include(":instrumentation:spring:spring-cloud-aws-3.0:javaagent") include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent") include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.2:testing") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-webflux-4.3:testing") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-webmvc-4.3:javaagent") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent") include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing") include(":instrumentation:spring:spring-core-2.0:javaagent") include(":instrumentation:spring:spring-data:spring-data-1.8:javaagent")