Skip to content

Commit 93d93b1

Browse files
jaydelucalaurit
andauthored
spring-cloud-gateway support for spring boot 4 (#15540)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 4b273aa commit 93d93b1

File tree

26 files changed

+653
-176
lines changed

26 files changed

+653
-176
lines changed

.fossa.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,12 @@ targets:
11381138
- type: gradle
11391139
path: ./
11401140
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent'
1141+
- type: gradle
1142+
path: ./
1143+
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent'
1144+
- type: gradle
1145+
path: ./
1146+
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-webmvc-4.3:javaagent'
11411147
- type: gradle
11421148
path: ./
11431149
target: ':instrumentation:spring:spring-data:spring-data-1.8:javaagent'

.github/scripts/check-javaagent-suppression-keys.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ for file in $(find instrumentation -name "*Module.java"); do
3333
# TODO module is missing a base version
3434
continue
3535
fi
36+
if [[ "$simple_module_name" == spring-cloud-gateway-webmvc ]]; then
37+
# webmvc variant uses spring-cloud-gateway as base name
38+
simple_module_name="spring-cloud-gateway"
39+
fi
3640

3741
if [ "$module_name" == "$simple_module_name" ]; then
3842
expected="super\(\n? *\"$simple_module_name\""

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,25 @@ muzzle {
77
pass {
88
group.set("org.springframework.cloud")
99
module.set("spring-cloud-starter-gateway")
10-
versions.set("[2.0.0.RELEASE,]")
10+
versions.set("[2.0.0.RELEASE,)")
11+
assertInverse.set(true)
12+
}
13+
14+
// Spring Cloud Gateway 4.3.0+ split into separate artifacts
15+
// see spring-cloud-starter-gateway-server-webmvc-4.3 for mvc
16+
pass {
17+
group.set("org.springframework.cloud")
18+
module.set("spring-cloud-starter-gateway-server-webflux")
19+
versions.set("[4.3.0,]")
1120
assertInverse.set(true)
1221
}
1322
}
1423

1524
dependencies {
1625
library("org.springframework.cloud:spring-cloud-starter-gateway:2.0.0.RELEASE")
1726

27+
implementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent"))
28+
1829
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
1930
testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent"))
2031
testInstrumentation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent"))

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0;
77

8-
import static java.util.Arrays.asList;
8+
import static java.util.Collections.singletonList;
99

1010
import com.google.auto.service.AutoService;
1111
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
@@ -23,7 +23,7 @@ public GatewayInstrumentationModule() {
2323

2424
@Override
2525
public List<TypeInstrumentation> typeInstrumentations() {
26-
return asList(new HandlerAdapterInstrumentation());
26+
return singletonList(new HandlerAdapterInstrumentation());
2727
}
2828

2929
@Override

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java

Lines changed: 7 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,28 @@
55

66
package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0;
77

8+
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_FILTER_SIZE_ATTRIBUTE;
9+
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ID_ATTRIBUTE;
10+
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ORDER_ATTRIBUTE;
11+
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_URI_ATTRIBUTE;
812
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
913

10-
import io.opentelemetry.api.common.AttributeKey;
1114
import io.opentelemetry.api.trace.Span;
1215
import io.opentelemetry.context.Context;
1316
import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
14-
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
15-
import java.util.regex.Pattern;
17+
import io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper;
1618
import javax.annotation.Nullable;
1719
import org.springframework.cloud.gateway.route.Route;
1820
import org.springframework.web.server.ServerWebExchange;
1921

2022
public final class ServerWebExchangeHelper {
2123

22-
/** Route ID attribute key. */
23-
private static final AttributeKey<String> ROUTE_ID_ATTRIBUTE =
24-
AttributeKey.stringKey("spring-cloud-gateway.route.id");
25-
26-
/** Route URI attribute key. */
27-
private static final AttributeKey<String> ROUTE_URI_ATTRIBUTE =
28-
AttributeKey.stringKey("spring-cloud-gateway.route.uri");
29-
30-
/** Route order attribute key. */
31-
private static final AttributeKey<Long> ROUTE_ORDER_ATTRIBUTE =
32-
AttributeKey.longKey("spring-cloud-gateway.route.order");
33-
34-
/** Route filter size attribute key. */
35-
private static final AttributeKey<Long> ROUTE_FILTER_SIZE_ATTRIBUTE =
36-
AttributeKey.longKey("spring-cloud-gateway.route.filter.size");
37-
38-
private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES;
39-
40-
static {
41-
CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES =
42-
AgentInstrumentationConfig.get()
43-
.getBoolean(
44-
"otel.instrumentation.spring-cloud-gateway.experimental-span-attributes", false);
45-
}
46-
47-
/* Regex for UUID */
48-
private static final Pattern UUID_REGEX =
49-
Pattern.compile(
50-
"^[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}$");
51-
52-
private static final String INVALID_RANDOM_ROUTE_ID =
53-
"org.springframework.util.AlternativeJdkIdGenerator@";
54-
5524
private ServerWebExchangeHelper() {}
5625

5726
public static void extractAttributes(ServerWebExchange exchange, Context context) {
5827
// Record route info
5928
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
60-
if (route != null && CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) {
29+
if (route != null && GatewayRouteHelper.shouldCaptureExperimentalSpanAttributes()) {
6130
Span serverSpan = LocalRootSpan.fromContextOrNull(context);
6231
if (serverSpan == null) {
6332
return;
@@ -76,30 +45,8 @@ public static String extractServerRoute(@Nullable ServerWebExchange exchange) {
7645
}
7746
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
7847
if (route != null) {
79-
return convergeRouteId(route);
48+
return GatewayRouteHelper.convergeRouteId(route.getId());
8049
}
8150
return null;
8251
}
83-
84-
/**
85-
* To avoid high cardinality, we ignore random UUID generated by Spring Cloud Gateway. Spring
86-
* Cloud Gateway generate invalid random routeID, and it is fixed until 3.1.x
87-
*
88-
* @see <a
89-
* href="https://github.com/spring-cloud/spring-cloud-gateway/commit/5002fe2e0a2825ef47dd667cade37b844c276cf6"/>
90-
*/
91-
@Nullable
92-
private static String convergeRouteId(Route route) {
93-
String routeId = route.getId();
94-
if (routeId == null || routeId.isEmpty()) {
95-
return null;
96-
}
97-
if (UUID_REGEX.matcher(routeId).matches()) {
98-
return null;
99-
}
100-
if (routeId.startsWith(INVALID_RANDOM_ROUTE_ID)) {
101-
return null;
102-
}
103-
return routeId;
104-
}
10552
}

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java

Lines changed: 17 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,73 +5,36 @@
55

66
package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0;
77

8-
import static org.assertj.core.api.Assertions.assertThat;
9-
10-
import io.opentelemetry.api.trace.SpanKind;
118
import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest;
12-
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
13-
import org.junit.jupiter.api.Test;
9+
import io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
10+
import java.util.List;
1411
import org.junit.jupiter.api.extension.ExtendWith;
1512
import org.springframework.boot.test.context.SpringBootTest;
1613
import org.springframework.test.context.junit.jupiter.SpringExtension;
1714

1815
@ExtendWith(SpringExtension.class)
1916
@SpringBootTest(
2017
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
21-
classes = {
22-
GatewayTestApplication.class,
23-
GatewayRouteMappingTest.ForceNettyAutoConfiguration.class
24-
})
18+
classes = {GatewayTestApplication.class})
2519
class GatewayRouteMappingTest extends AbstractRouteMappingTest {
2620

27-
@Test
28-
void gatewayRouteMappingTest() {
29-
String requestBody = "gateway";
30-
AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join();
31-
assertThat(response.status().code()).isEqualTo(200);
32-
assertThat(response.contentUtf8()).isEqualTo(requestBody);
33-
testing.waitAndAssertTraces(
34-
trace ->
35-
trace.hasSpansSatisfyingExactly(
36-
span ->
37-
span.hasName("POST path_route")
38-
.hasKind(SpanKind.SERVER)
39-
.hasAttributesSatisfying(
40-
buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1)),
41-
span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL)));
21+
@Override
22+
protected String getSpanName() {
23+
return "POST path_route";
24+
}
25+
26+
@Override
27+
protected List<AttributeAssertion> getExpectedAttributes() {
28+
return buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1);
4229
}
4330

44-
@Test
45-
void gatewayRandomUuidRouteMappingTest() {
46-
String requestBody = "gateway";
47-
AggregatedHttpResponse response = client.post("/uuid/echo", requestBody).aggregate().join();
48-
assertThat(response.status().code()).isEqualTo(200);
49-
assertThat(response.contentUtf8()).isEqualTo(requestBody);
50-
testing.waitAndAssertTraces(
51-
trace ->
52-
trace.hasSpansSatisfyingExactly(
53-
span ->
54-
span.hasName("POST")
55-
.hasKind(SpanKind.SERVER)
56-
.hasAttributesSatisfying(buildAttributeAssertions("h1c://mock.uuid", 0, 1)),
57-
span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL)));
31+
@Override
32+
protected List<AttributeAssertion> getRandomUuidExpectedAttributes() {
33+
return buildAttributeAssertions("h1c://mock.uuid", 0, 1);
5834
}
5935

60-
@Test
61-
void gatewayFakeUuidRouteMappingTest() {
62-
String requestBody = "gateway";
63-
String routeId = "ffffffff-ffff-ffff-ffff-ffff";
64-
AggregatedHttpResponse response = client.post("/fake/echo", requestBody).aggregate().join();
65-
assertThat(response.status().code()).isEqualTo(200);
66-
assertThat(response.contentUtf8()).isEqualTo(requestBody);
67-
testing.waitAndAssertTraces(
68-
trace ->
69-
trace.hasSpansSatisfyingExactly(
70-
span ->
71-
span.hasName("POST " + routeId)
72-
.hasKind(SpanKind.SERVER)
73-
.hasAttributesSatisfying(
74-
buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1)),
75-
span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL)));
36+
@Override
37+
protected List<AttributeAssertion> getFakeUuidExpectedAttributes(String routeId) {
38+
return buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1);
7639
}
7740
}

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ dependencies {
1414
testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE")
1515
testLibrary("org.springframework.boot:spring-boot-starter-test:2.2.0.RELEASE")
1616

17-
// tests don't work with spring boot 4 yet
18-
latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // documented limitation
17+
latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:3.+") // see spring-cloud-gateway-4.3* module
1918
}
2019

20+
val latestDepTest = findProperty("testLatestDeps") as Boolean
21+
2122
tasks.withType<Test>().configureEach {
2223
jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true")
2324

@@ -27,11 +28,9 @@ tasks.withType<Test>().configureEach {
2728

2829
jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true")
2930

30-
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
31+
systemProperty("testLatestDeps", latestDepTest)
3132
}
3233

33-
val latestDepTest = findProperty("testLatestDeps") as Boolean
34-
3534
if (latestDepTest) {
3635
// spring 6 requires java 17
3736
otelJava {

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,10 @@
55

66
package io.opentelemetry.instrumentation.spring.gateway.v2_2;
77

8-
import static org.assertj.core.api.Assertions.assertThat;
9-
10-
import io.opentelemetry.api.trace.SpanKind;
118
import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest;
12-
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
13-
import org.junit.jupiter.api.Test;
149
import org.springframework.boot.test.context.SpringBootTest;
1510

1611
@SpringBootTest(
1712
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
18-
classes = {
19-
Gateway22TestApplication.class,
20-
Gateway22RouteMappingTest.ForceNettyAutoConfiguration.class
21-
})
22-
class Gateway22RouteMappingTest extends AbstractRouteMappingTest {
23-
24-
@Test
25-
void gatewayRouteMappingTest() {
26-
String requestBody = "gateway";
27-
AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join();
28-
assertThat(response.status().code()).isEqualTo(200);
29-
assertThat(response.contentUtf8()).isEqualTo(requestBody);
30-
testing.waitAndAssertTraces(
31-
trace ->
32-
trace.hasSpansSatisfyingExactly(
33-
span ->
34-
span.hasName("POST")
35-
.hasKind(SpanKind.SERVER)
36-
.hasAttributesSatisfying(
37-
// Global filter is not route filter, so filter size should be 0.
38-
buildAttributeAssertions("h1c://mock.response", 2023, 0)),
39-
span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL)));
40-
}
41-
}
13+
classes = {Gateway22TestApplication.class})
14+
class Gateway22RouteMappingTest extends AbstractRouteMappingTest {}

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,8 @@
55

66
package io.opentelemetry.instrumentation.spring.gateway.v2_2;
77

8+
import io.opentelemetry.instrumentation.spring.gateway.common.GatewayTestApplication;
89
import org.springframework.boot.autoconfigure.SpringBootApplication;
9-
import org.springframework.cloud.gateway.filter.GlobalFilter;
10-
import org.springframework.context.annotation.Bean;
1110

1211
@SpringBootApplication
13-
public class Gateway22TestApplication {
14-
@Bean
15-
public GlobalFilter echoFilter() {
16-
return (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody());
17-
}
18-
}
12+
public class Gateway22TestApplication extends GatewayTestApplication {}

instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,14 @@ spring:
66
predicates:
77
- Path=/gateway/echo
88
order: 2023
9+
# Route without ID - will get random UUID (should be filtered out)
10+
- uri: h1c://mock.uuid
11+
predicates:
12+
- Path=/uuid/echo
13+
order: 0
14+
# Route with fake UUID ID (should NOT be filtered out)
15+
- id: ffffffff-ffff-ffff-ffff-ffff
16+
uri: h1c://mock.fake
17+
predicates:
18+
- Path=/fake/echo
19+
order: 0

0 commit comments

Comments
 (0)