diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/build.gradle b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/build.gradle
index 95fde4e327f..11e6c99222a 100644
--- a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/build.gradle
+++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/build.gradle
@@ -4,7 +4,7 @@ muzzle {
module = 'spring-webmvc'
versions = "[6,)"
javaVersion = "17"
- extraDependency "jakarta.servlet:jakarta.servlet-api:5.0.0"
+ extraDependency "jakarta.servlet:jakarta.servlet-api:6.1.0"
}
}
@@ -55,10 +55,10 @@ dependencies {
testImplementation(libs.spock.spring)
- latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.+'
- latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '3.+'
- latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.+'
- latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '3.+'
+ latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '4.+'
+ latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '4.+'
+ latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '4.+'
+ latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '4.+'
}
diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecorator.java b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecorator.java
index 79b89c1aad5..83fca460611 100644
--- a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecorator.java
+++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecorator.java
@@ -101,6 +101,11 @@ protected int status(final HttpServletResponse httpServletResponse) {
return httpServletResponse.getStatus();
}
+ // TODO Switch to HandlerMapping.API_VERSION_ATTRIBUTE once compile baseline moves to Spring
+ // Framework 7.
+ private static final String API_VERSION_ATTRIBUTE =
+ "org.springframework.web.servlet.HandlerMapping.apiVersion";
+
@Override
public AgentSpan onRequest(
final AgentSpan span,
@@ -117,6 +122,10 @@ public AgentSpan onRequest(
request.setAttribute(DD_FILTERED_SPRING_ROUTE_ALREADY_APPLIED, true);
HTTP_RESOURCE_DECORATOR.withRoute(span, method, bestMatchingPattern.toString());
}
+ final Object apiVersion = request.getAttribute(API_VERSION_ATTRIBUTE);
+ if (apiVersion instanceof String && !((String) apiVersion).isEmpty()) {
+ span.setTag("http.api_version", (String) apiVersion);
+ }
}
return span;
}
diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorApiVersionTest.java b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorApiVersionTest.java
new file mode 100644
index 00000000000..9a8ad7f49a7
--- /dev/null
+++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorApiVersionTest.java
@@ -0,0 +1,109 @@
+package datadog.trace.instrumentation.springweb6;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+/**
+ * Tests for the {@code http.api_version} span tag added by {@link SpringWebHttpServerDecorator}.
+ *
+ *
Unlike {@link SpringWebHttpServerDecoratorTest}, these tests use Spring's {@link
+ * MockHttpServletRequest} to supply a realistic request object and record tag writes via a
+ * recording answer on the span mock — asserting on the actual stored tag value rather
+ * than just verifying that {@code setTag} was called.
+ */
+class SpringWebHttpServerDecoratorApiVersionTest {
+
+ private static final String API_VERSION_ATTRIBUTE =
+ "org.springframework.web.servlet.HandlerMapping.apiVersion";
+
+ private AgentSpan span;
+ private Map capturedTags;
+
+ @BeforeEach
+ void setup() {
+ capturedTags = new HashMap<>();
+ span = mock(AgentSpan.class);
+ when(span.setTag(anyString(), anyString())).thenAnswer(this::captureTag);
+ }
+
+ private AgentSpan captureTag(InvocationOnMock invocation) {
+ capturedTags.put(invocation.getArgument(0), invocation.getArgument(1));
+ return span;
+ }
+
+ /**
+ * When the request carries the Spring Framework 7 api-version attribute, {@code
+ * SpringWebHttpServerDecorator.onRequest()} must write {@code http.api_version} on the span and
+ * the stored value must equal the attribute string.
+ */
+ @Test
+ void onRequest_setsHttpApiVersionTag_whenAttributePresent() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/items");
+ request.setAttribute(API_VERSION_ATTRIBUTE, "v2");
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ assertEquals(
+ "v2",
+ capturedTags.get("http.api_version"),
+ "http.api_version tag must reflect the value of the HandlerMapping.apiVersion attribute");
+ }
+
+ /**
+ * When the attribute is absent the tag must not be written at all — a missing key in {@code
+ * capturedTags} is the proof.
+ */
+ @Test
+ void onRequest_doesNotSetHttpApiVersionTag_whenAttributeAbsent() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/items");
+ // No API_VERSION_ATTRIBUTE set
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ assertNull(
+ capturedTags.get("http.api_version"),
+ "http.api_version must not be tagged when the attribute is missing");
+ }
+
+ /**
+ * An empty string is treated the same as absent: no tag must be written.
+ */
+ @Test
+ void onRequest_doesNotSetHttpApiVersionTag_whenAttributeIsEmpty() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/items");
+ request.setAttribute(API_VERSION_ATTRIBUTE, "");
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ assertNull(
+ capturedTags.get("http.api_version"),
+ "http.api_version must not be tagged when the attribute value is empty");
+ }
+
+ /**
+ * A non-String attribute (e.g., an enum or domain object) must not cause an exception and must
+ * not write the tag.
+ */
+ @Test
+ void onRequest_doesNotSetHttpApiVersionTag_whenAttributeIsNonString() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/items");
+ request.setAttribute(API_VERSION_ATTRIBUTE, Integer.valueOf(2));
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ assertNull(
+ capturedTags.get("http.api_version"),
+ "http.api_version must not be tagged when the attribute is a non-String type");
+ }
+}
diff --git a/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorTest.java b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorTest.java
new file mode 100644
index 00000000000..dd037110dc3
--- /dev/null
+++ b/dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/java/datadog/trace/instrumentation/springweb6/SpringWebHttpServerDecoratorTest.java
@@ -0,0 +1,54 @@
+package datadog.trace.instrumentation.springweb6;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.Test;
+
+class SpringWebHttpServerDecoratorTest {
+
+ private static final String API_VERSION_ATTRIBUTE =
+ "org.springframework.web.servlet.HandlerMapping.apiVersion";
+
+ @Test
+ void setsHttpApiVersionTagWhenAttributePresent() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ AgentSpan span = mock(AgentSpan.class);
+ when(request.getMethod()).thenReturn("GET");
+ when(request.getAttribute(API_VERSION_ATTRIBUTE)).thenReturn("v1");
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ verify(span).setTag("http.api_version", "v1");
+ }
+
+ @Test
+ void doesNotSetHttpApiVersionTagWhenAttributeAbsent() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ AgentSpan span = mock(AgentSpan.class);
+ when(request.getMethod()).thenReturn("GET");
+ when(request.getAttribute(API_VERSION_ATTRIBUTE)).thenReturn(null);
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ verify(span, never()).setTag(eq("http.api_version"), anyString());
+ }
+
+ @Test
+ void doesNotSetHttpApiVersionTagWhenAttributeEmpty() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ AgentSpan span = mock(AgentSpan.class);
+ when(request.getMethod()).thenReturn("GET");
+ when(request.getAttribute(API_VERSION_ATTRIBUTE)).thenReturn("");
+
+ SpringWebHttpServerDecorator.DECORATE.onRequest(span, request, request, null);
+
+ verify(span, never()).setTag(eq("http.api_version"), anyString());
+ }
+}