-
Notifications
You must be signed in to change notification settings - Fork 828
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: get route info in spring-cloud-gateway (#9597)
- Loading branch information
1 parent
11cac29
commit 702ae30
Showing
17 changed files
with
608 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | |
51 changes: 51 additions & 0 deletions
51
...mentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
...telemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.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; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
...ava/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
...elemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
.../opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
.../opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))); | ||
} | ||
} |
Oops, something went wrong.