feat: get route info in spring-cloud-gateway (#9597)
This commit is contained in:
parent
11cac29ba7
commit
702ae30076
|
@ -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] |
|
||||
|
|
|
@ -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. |
|
|
@ -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<Test>().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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TypeInstrumentation> typeInstrumentations() {
|
||||
return asList(new HandlerAdapterInstrumentation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
// Later than Spring Webflux.
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -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<ServerWebExchange> httpRouteGetter() {
|
||||
return (context, exchange) -> ServerWebExchangeHelper.extractServerRoute(exchange);
|
||||
}
|
||||
}
|
|
@ -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<ClassLoader> classLoaderOptimization() {
|
||||
return hasClassesNamed("org.springframework.web.reactive.HandlerAdapter");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementMatcher<TypeDescription> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> ROUTE_ID_ATTRIBUTE =
|
||||
AttributeKey.stringKey("spring-cloud-gateway.route.id");
|
||||
|
||||
/** Route URI attribute key. */
|
||||
private static final AttributeKey<String> ROUTE_URI_ATTRIBUTE =
|
||||
AttributeKey.stringKey("spring-cloud-gateway.route.uri");
|
||||
|
||||
/** Route order attribute key. */
|
||||
private static final AttributeKey<Long> ROUTE_ORDER_ATTRIBUTE =
|
||||
AttributeKey.longKey("spring-cloud-gateway.route.order");
|
||||
|
||||
/** Route filter size attribute key. */
|
||||
private static final AttributeKey<Long> 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 <a
|
||||
* href="https://github.com/spring-cloud/spring-cloud-gateway/commit/5002fe2e0a2825ef47dd667cade37b844c276cf6"/>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="console"/>
|
||||
</root>
|
||||
|
||||
<!-- this is needed because when Spring Boot starts it overrides the debug log level that was
|
||||
configured in AgentTestRunner -->
|
||||
<logger name="io.opentelemetry" level="debug"/>
|
||||
|
||||
</configuration>
|
|
@ -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<Test>().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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- uri: h1c://mock.response
|
||||
predicates:
|
||||
- Path=/gateway/echo
|
||||
order: 2023
|
|
@ -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")
|
||||
}
|
|
@ -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<AttributeAssertion> buildAttributeAssertions(
|
||||
String routeId, String uri, int order, int filterSize) {
|
||||
List<AttributeAssertion> 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<AttributeAssertion> buildAttributeAssertions(
|
||||
String uri, int order, int filterSize) {
|
||||
return buildAttributeAssertions(null, uri, order, filterSize);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue