Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,12 @@ targets:
- type: gradle
path: ./
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent'
- type: gradle
path: ./
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:javaagent'
- type: gradle
path: ./
target: ':instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-webmvc-4.3:javaagent'
- type: gradle
path: ./
target: ':instrumentation:spring:spring-data:spring-data-1.8:javaagent'
Expand Down
4 changes: 4 additions & 0 deletions .github/scripts/check-javaagent-suppression-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ for file in $(find instrumentation -name "*Module.java"); do
# TODO module is missing a base version
continue
fi
if [[ "$simple_module_name" == spring-cloud-gateway-webmvc ]]; then
# webmvc variant uses spring-cloud-gateway as base name
simple_module_name="spring-cloud-gateway"
fi

if [ "$module_name" == "$simple_module_name" ]; then
expected="super\(\n? *\"$simple_module_name\""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ muzzle {
pass {
group.set("org.springframework.cloud")
module.set("spring-cloud-starter-gateway")
versions.set("[2.0.0.RELEASE,]")
versions.set("[2.0.0.RELEASE,)")
assertInverse.set(true)
}

// Spring Cloud Gateway 4.3.0+ split into separate artifacts
// see spring-cloud-starter-gateway-server-webmvc-4.3 for mvc
pass {
group.set("org.springframework.cloud")
module.set("spring-cloud-starter-gateway-server-webflux")
versions.set("[4.3.0,]")
assertInverse.set(true)
}
}

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

implementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common: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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;

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

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new HandlerAdapterInstrumentation());
return singletonList(new HandlerAdapterInstrumentation());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,28 @@

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

import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_FILTER_SIZE_ATTRIBUTE;
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ID_ATTRIBUTE;
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_ORDER_ATTRIBUTE;
import static io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper.ROUTE_URI_ATTRIBUTE;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
import java.util.regex.Pattern;
import io.opentelemetry.javaagent.instrumentation.spring.gateway.common.GatewayRouteHelper;
import javax.annotation.Nullable;
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 =
AgentInstrumentationConfig.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) {
if (route != null && GatewayRouteHelper.shouldCaptureExperimentalSpanAttributes()) {
Span serverSpan = LocalRootSpan.fromContextOrNull(context);
if (serverSpan == null) {
return;
Expand All @@ -76,30 +45,8 @@ public static String extractServerRoute(@Nullable ServerWebExchange exchange) {
}
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route != null) {
return convergeRouteId(route);
return GatewayRouteHelper.convergeRouteId(route.getId());
}
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"/>
*/
@Nullable
private static String convergeRouteId(Route route) {
String routeId = route.getId();
if (routeId == null || routeId.isEmpty()) {
return null;
}
if (UUID_REGEX.matcher(routeId).matches()) {
return null;
}
if (routeId.startsWith(INVALID_RANDOM_ROUTE_ID)) {
return null;
}
return routeId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,36 @@

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 io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
import java.util.List;
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
})
classes = {GatewayTestApplication.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)));
@Override
protected String getSpanName() {
return "POST path_route";
}

@Override
protected List<AttributeAssertion> getExpectedAttributes() {
return buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1);
}

@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)));
@Override
protected List<AttributeAssertion> getRandomUuidExpectedAttributes() {
return buildAttributeAssertions("h1c://mock.uuid", 0, 1);
}

@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)));
@Override
protected List<AttributeAssertion> getFakeUuidExpectedAttributes(String routeId) {
return buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ dependencies {
testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE")
testLibrary("org.springframework.boot:spring-boot-starter-test:2.2.0.RELEASE")

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

val latestDepTest = findProperty("testLatestDeps") as Boolean

tasks.withType<Test>().configureEach {
jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true")

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

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

systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
systemProperty("testLatestDeps", latestDepTest)
}

val latestDepTest = findProperty("testLatestDeps") as Boolean

if (latestDepTest) {
// spring 6 requires java 17
otelJava {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,10 @@

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.springframework.boot.test.context.SpringBootTest;

@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)));
}
}
classes = {Gateway22TestApplication.class})
class Gateway22RouteMappingTest extends AbstractRouteMappingTest {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@

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

import io.opentelemetry.instrumentation.spring.gateway.common.GatewayTestApplication;
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());
}
}
public class Gateway22TestApplication extends GatewayTestApplication {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,14 @@ spring:
predicates:
- Path=/gateway/echo
order: 2023
# Route without ID - will get random UUID (should be filtered out)
- uri: h1c://mock.uuid
predicates:
- Path=/uuid/echo
order: 0
# Route with fake UUID ID (should NOT be filtered out)
- id: ffffffff-ffff-ffff-ffff-ffff
uri: h1c://mock.fake
predicates:
- Path=/fake/echo
order: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.javaagent-instrumentation")
}
Loading
Loading