Skip to content

Commit

Permalink
feat: get route info in spring-cloud-gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
123liuziming committed Oct 2, 2023
1 parent d0523fd commit bbd5d11
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
id("otel.javaagent-instrumentation")
}

dependencies {
compileOnly("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"))

testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.0.0.RELEASE")
testLibrary("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE")
}

tasks.withType<Test>().configureEach {
// TODO run tests both with and without experimental span attributes
jvmArgs("-Dotel.instrumentation.spring-webflux.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")
}
}
}
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;

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.spring.gateway;

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);
}
}
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;

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 current span, should be webflux's span.
ServerWebExchangeHelper.extractAttributes(exchange, context);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.spring.gateway;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.web.server.ServerWebExchange;

public final class ServerWebExchangeHelper {

/**
* Route info key.
*/
public static final String ROUTE_INFO_ATTRIBUTES = "ROUTE_INFO";


private ServerWebExchangeHelper() {

}

public static void extractAttributes(ServerWebExchange exchange, Context context) {
// Record route info
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route != null) {
Span currentSpan = Span.fromContext(context);
assert currentSpan != null;
currentSpan.setAttribute(ROUTE_INFO_ATTRIBUTES, route.toString());
}
}


public static String extractServerRoute(ServerWebExchange exchange) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route != null) {
return "Route@" + route.getId();
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.opentelemetry.javaagent.instrumentation.spring.gateway;/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.semconv.SemanticAttributes;
import io.opentelemetry.testing.internal.armeria.client.WebClient;
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {
GatewayTestApplication.class,
GatewayRouteMappingTest.ForceNettyAutoConfiguration.class
})
public class GatewayRouteMappingTest {

@TestConfiguration
static class ForceNettyAutoConfiguration {
@Bean
NettyReactiveWebServerFactory nettyFactory() {
return new NettyReactiveWebServerFactory();
}
}

@RegisterExtension
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();

@Value("${local.server.port}")
private int port;

private WebClient client;

@BeforeEach
void beforeEach() {
client = WebClient.builder("h1c://localhost:" + port).followRedirects().build();
}

@Test
void gatewayRouteMappingTest() {
String requestBody = "gateway";
String expectRoute = "Route@path_route";
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.hasAttribute(
equalTo(SemanticAttributes.HTTP_ROUTE, expectRoute)),
span -> span.hasAttributesSatisfying(
satisfies(AttributeKey.stringKey(ServerWebExchangeHelper.ROUTE_INFO_ATTRIBUTES),
s -> s.contains("id='path_route'")),
satisfies(AttributeKey.stringKey(ServerWebExchangeHelper.ROUTE_INFO_ATTRIBUTES),
s -> s.contains("uri=h1c://mock.response")
)
)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.spring.gateway;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
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(f -> f.filter(
(exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody()))).uri("h1c://mock.response"))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ include(":instrumentation:spring:spring-boot-autoconfigure")
include(":instrumentation:spring:starters:spring-boot-starter")
include(":instrumentation:spring:starters:jaeger-spring-boot-starter")
include(":instrumentation:spring:starters:zipkin-spring-boot-starter")
include(":instrumentation:spring:spring-cloud-gateway:javaagent")
include(":instrumentation:spymemcached-2.12:javaagent")
include(":instrumentation:struts-2.3:javaagent")
include(":instrumentation:tapestry-5.4:javaagent")
Expand Down

0 comments on commit bbd5d11

Please sign in to comment.