diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 29c10880ecc9..8491b4338208 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -114,6 +114,7 @@ These are the supported libraries and frameworks: | [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | N/A | Provides `http.route` [2] | | [Spring Boot](https://spring.io/projects/spring-boot) | | [opentelemetry-spring-boot-resources](../instrumentation/spring/spring-boot-resources/library) | none | | [Spring Batch](https://spring.io/projects/spring-batch) | 3.0+ (not including 5.0+ yet) | N/A | none | +| [Spring Cloud Gateway](https://github.com/spring-cloud/spring-cloud-gateway) | 2.0+ | N/A | Provides `http.route` [2] | | [Spring Data](https://spring.io/projects/spring-data) | 1.8+ | N/A | none | | [Spring Integration](https://spring.io/projects/spring-integration) | 4.1+ (not including 6.0+ yet) | [opentelemetry-spring-integration-4.1](../instrumentation/spring/spring-integration-4.1/library) | [Messaging Spans] | | [Spring JMS](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#jms) | 2.0+ | N/A | [Messaging Spans] | diff --git a/instrumentation/spring/spring-cloud-gateway/README.md b/instrumentation/spring/spring-cloud-gateway/README.md new file mode 100644 index 000000000000..9da9be01f443 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/README.md @@ -0,0 +1,5 @@ +# Settings for the Spring Cloud Gateway instrumentation + +| System property | Type | Default | Description | +|--------------------------------------------------------------------------| ------- | ------- |---------------------------------------------------------------------------------------------| +| `otel.instrumentation.spring-cloud-gateway.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | 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 new file mode 100644 index 000000000000..b6e0f261e88a --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.springframework.cloud") + module.set("spring-cloud-starter-gateway") + versions.set("[2.0.0.RELEASE,]") + } +} + +dependencies { + library("org.springframework.cloud:spring-cloud-starter-gateway:2.0.0.RELEASE") + + 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")) + + testLibrary("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + // spring 6 requires java 17 + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) + } +} else { + // spring 5 requires old logback (and therefore also old slf4j) + configurations.testRuntimeClasspath { + resolutionStrategy { + force("ch.qos.logback:logback-classic:1.2.11") + force("org.slf4j:slf4j-api:1.7.36") + } + } +} 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 new file mode 100644 index 000000000000..c5b719af8f14 --- /dev/null +++ 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 @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GatewayInstrumentationModule extends InstrumentationModule { + + public GatewayInstrumentationModule() { + super("spring-cloud-gateway"); + } + + @Override + public List typeInstrumentations() { + return asList(new HandlerAdapterInstrumentation()); + } + + @Override + public int order() { + // Later than Spring Webflux. + return 1; + } +} 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/GatewaySingletons.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java new file mode 100644 index 000000000000..f52c910c1349 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerRouteGetter; +import org.springframework.web.server.ServerWebExchange; + +public final class GatewaySingletons { + + private GatewaySingletons() {} + + public static HttpServerRouteGetter httpRouteGetter() { + return (context, exchange) -> ServerWebExchangeHelper.extractServerRoute(exchange); + } +} 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/HandlerAdapterInstrumentation.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java new file mode 100644 index 000000000000..dc6870de0b30 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +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.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerRouteSource; +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.server.ServerWebExchange; + +public class HandlerAdapterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.reactive.HandlerAdapter"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isAbstract()) + .and(implementsInterface(named("org.springframework.web.reactive.HandlerAdapter"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handle")) + .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))) + .and(takesArgument(1, Object.class)) + .and(takesArguments(2)), + this.getClass().getName() + "$HandleAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter(@Advice.Argument(0) ServerWebExchange exchange) { + Context context = Context.current(); + // Update route info for server span. + HttpServerRoute.update( + context, + HttpServerRouteSource.NESTED_CONTROLLER, + GatewaySingletons.httpRouteGetter(), + exchange); + // Record route info in server span. + ServerWebExchangeHelper.extractAttributes(exchange, context); + } + } +} 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 new file mode 100644 index 000000000000..50ab67b0b04f --- /dev/null +++ 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 @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import java.util.regex.Pattern; +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 = + InstrumentationConfig.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) { + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + serverSpan.setAttribute(ROUTE_ID_ATTRIBUTE, route.getId()); + serverSpan.setAttribute(ROUTE_URI_ATTRIBUTE, route.getUri().toASCIIString()); + serverSpan.setAttribute(ROUTE_ORDER_ATTRIBUTE, route.getOrder()); + serverSpan.setAttribute(ROUTE_FILTER_SIZE_ATTRIBUTE, route.getFilters().size()); + } + } + + public static String extractServerRoute(ServerWebExchange exchange) { + Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + if (route != null) { + return convergeRouteId(route); + } + 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 + */ + private static String convergeRouteId(Route route) { + String routeId = route.getId(); + if (StringUtils.isNullOrEmpty(routeId)) { + 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 new file mode 100644 index 000000000000..d3a907ec959b --- /dev/null +++ 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 @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + GatewayTestApplication.class, + GatewayRouteMappingTest.ForceNettyAutoConfiguration.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))); + } + + @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))); + } + + @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))); + } +} 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/GatewayTestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java new file mode 100644 index 000000000000..6daef51e2d77 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.route.builder.UriSpec; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class GatewayTestApplication { + + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + // A simple echo gateway. + return builder + .routes() + .route( + "path_route", + r -> + r.path("/gateway/**") + .filters(GatewayTestApplication::echoFunc) + .uri("h1c://mock.response")) + // The routeID should be a random UUID. + .route( + r -> + r.path("/uuid/**").filters(GatewayTestApplication::echoFunc).uri("h1c://mock.uuid")) + // Seems like an uuid but not. + .route( + "ffffffff-ffff-ffff-ffff-ffff", + r -> + r.path("/fake/**").filters(GatewayTestApplication::echoFunc).uri("h1c://mock.fake")) + .build(); + } + + private static UriSpec echoFunc(GatewayFilterSpec f) { + return f.filter( + (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody())); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000000..7f2406629488 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..28dadbb0416e --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts @@ -0,0 +1,43 @@ +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")) + + testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE") + testLibrary("org.springframework.boot:spring-boot-starter-test:2.2.0.RELEASE") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + // spring 6 requires java 17 + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) + } +} else { + // spring 5 requires old logback (and therefore also old slf4j) + configurations.testRuntimeClasspath { + resolutionStrategy { + force("ch.qos.logback:logback-classic:1.2.11") + force("org.slf4j:slf4j-api:1.7.36") + } + } +} 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 new file mode 100644 index 000000000000..2017cc2ace9c --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@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))); + } +} 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 new file mode 100644 index 000000000000..b8fd00860440 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.v2_2; + +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()); + } +} 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 new file mode 100644 index 000000000000..495bde2f5c16 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + cloud: + gateway: + routes: + - uri: h1c://mock.response + predicates: + - Path=/gateway/echo + order: 2023 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 new file mode 100644 index 000000000000..0a491303fd0c --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) + + compileOnly("org.springframework.boot:spring-boot-starter-test:2.0.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 new file mode 100644 index 000000000000..2d6f4d398c33 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.common; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.internal.StringUtils; +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 java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +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; + +public abstract class AbstractRouteMappingTest { + @TestConfiguration + public static class ForceNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Value("${local.server.port}") + private int port; + + protected WebClient client; + + protected static final String WEBFLUX_SPAN_NAME = "FilteringWebHandler.handle"; + + @BeforeEach + void beforeEach() { + client = WebClient.builder("h1c://localhost:" + port).followRedirects().build(); + } + + protected List buildAttributeAssertions( + String routeId, String uri, int order, int filterSize) { + List assertions = new ArrayList<>(); + if (!StringUtils.isNullOrEmpty(routeId)) { + assertions.add(equalTo(AttributeKey.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)); + return assertions; + } + + protected List buildAttributeAssertions( + String uri, int order, int filterSize) { + return buildAttributeAssertions(null, uri, order, filterSize); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 5d86cf6e9e18..caa039da5706 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -494,6 +494,9 @@ include(":instrumentation:spring:spring-batch-3.0:javaagent") include(":instrumentation:spring:spring-boot-actuator-autoconfigure-2.0:javaagent") include(":instrumentation:spring:spring-boot-resources:library") include(":instrumentation:spring:spring-boot-resources:testing") +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-common:testing") include(":instrumentation:spring:spring-core-2.0:javaagent") include(":instrumentation:spring:spring-data:spring-data-1.8:javaagent") include(":instrumentation:spring:spring-data:spring-data-3.0:testing")