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