From 03def017ee0599ed5dedd0c0eecbd9e724369f6e Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 27 Oct 2026 15:42:56 -0400 Subject: [PATCH 1/3] Keep the request's resource when Undertow forwards internally A servlet forward re-entered ServletInitialHandler.dispatchRequest and the advice applied the forward target as the route, renaming the root span (e.g. to the JSP being rendered) and overwriting servlet.path. If an earlier outgoing call had already triggered sampling, the trace kept the rate of the original resource's rule while being reported under the forward target. Only apply the route and servlet.path on REQUEST dispatches; forward, include, error and async dispatches happen inside the original request. Co-Authored-By: Claude Opus 5.5 --- .../undertow/ServletInstrumentation.java | 35 +++++++++++-------- .../src/test/groovy/ForwardingServlet.groovy | 13 +++++++ .../src/test/groovy/IncludingServlet.groovy | 13 +++++++ .../test/groovy/UndertowServletTest.groovy | 33 +++++++++++++++++ .../JakartaServletInstrumentation.java | 10 ++++-- .../src/test/groovy/ForwardingServlet.groovy | 13 +++++++ .../src/test/groovy/IncludingServlet.groovy | 13 +++++++ .../test/groovy/UndertowServletTest.groovy | 34 ++++++++++++++++++ 8 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java index 6e9877d6347..731746b18a0 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java @@ -18,6 +18,7 @@ import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletPathMatch; import io.undertow.servlet.handlers.ServletRequestContext; +import javax.servlet.DispatcherType; import javax.servlet.ServletRequest; import javax.servlet.http.MappingMatch; import net.bytebuddy.asm.Advice; @@ -64,7 +65,8 @@ public static class DispatchAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void enter( @Advice.Argument(0) final HttpServerExchange exchange, - @Advice.Argument(1) final ServletRequestContext servletRequestContext) { + @Advice.Argument(1) final ServletRequestContext servletRequestContext, + @Advice.Argument(3) final DispatcherType dispatcherType) { ContextContinuation continuation = exchange.getAttachment(DATADOG_UNDERTOW_CONTINUATION); if (continuation != null) { AgentSpan undertowSpan = spanFromContext(continuation.context()); @@ -73,21 +75,26 @@ public static void enter( undertowSpan.setSpanName(SERVLET_REQUEST); undertowSpan.setTag(SERVLET_CONTEXT, request.getServletContext().getContextPath()); - String relativePath = exchange.getRelativePath(); + // Forward, include, error and async dispatches happen inside the original request, so they + // must not rename it: the root span keeps describing what the client asked for. + if (dispatcherType == DispatcherType.REQUEST) { + String relativePath = exchange.getRelativePath(); - ServletPathMatch servletPathMatch = servletRequestContext.getServletPathMatch(); - if (UndertowDecorator.UNDERTOW_LEGACY_TRACING - && servletPathMatch != null - && servletPathMatch.getMappingMatch() != MappingMatch.DEFAULT) { - // Set the route unless the mapping match is default, this way we prevent setting route - // for a non-existing resource. Otherwise, it'd set a non-existing resource name with - // higher priority than 404 resource, so it wouldn't be able to set resource 404 later in - // the onResponse instrumentation. - HTTP_RESOURCE_DECORATOR.withRoute( - undertowSpan, exchange.getRequestMethod().toString(), relativePath, false); + ServletPathMatch servletPathMatch = servletRequestContext.getServletPathMatch(); + if (UndertowDecorator.UNDERTOW_LEGACY_TRACING + && servletPathMatch != null + && servletPathMatch.getMappingMatch() != MappingMatch.DEFAULT) { + // Set the route unless the mapping match is default, this way we prevent setting route + // for a non-existing resource. Otherwise, it'd set a non-existing resource name with + // higher priority than 404 resource, so it wouldn't be able to set resource 404 later + // in + // the onResponse instrumentation. + HTTP_RESOURCE_DECORATOR.withRoute( + undertowSpan, exchange.getRequestMethod().toString(), relativePath, false); + } + // The servlet.path tag is expected even for a non-existing resource. + undertowSpan.setTag(SERVLET_PATH, relativePath); } - // The servlet.path tag is expected even for a non-existing resource. - undertowSpan.setTag(SERVLET_PATH, relativePath); } } } diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy new file mode 100644 index 00000000000..5a4a3d4063e --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy @@ -0,0 +1,13 @@ +import javax.servlet.ServletException +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class ForwardingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.path).forward(req, resp) + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy new file mode 100644 index 00000000000..8b5fc514274 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy @@ -0,0 +1,13 @@ +import javax.servlet.ServletException +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class IncludingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.path).include(req, resp) + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy index 54b39c886c0..786c4ec14a6 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy @@ -14,6 +14,7 @@ import io.undertow.servlet.api.ServletContainer import io.undertow.servlet.api.ServletInfo import io.undertow.websockets.jsr.WebSocketDeploymentInfo import okhttp3.MediaType +import okhttp3.Request import okhttp3.RequestBody import spock.lang.IgnoreIf @@ -45,6 +46,8 @@ import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.WEBSOC abstract class UndertowServletTest extends HttpServerTest { private static final CONTEXT = "ctx" + private static final String FORWARDING_PATH = "/dispatch/forward" + private static final String INCLUDING_PATH = "/dispatch/include" private final static String MULTIPART_CONTENT_TYPE = 'multipart/form-data; charset=utf-8; boundary=------------------------943d3207457896a3' @@ -70,6 +73,8 @@ abstract class UndertowServletTest extends HttpServerTest { .setDeploymentName("servletContext.war") .addServlet(new ServletInfo("SuccessServlet", SuccessServlet).addMapping(SUCCESS.getPath())) .addServlet(new ServletInfo("ForwardedServlet", ForwardedServlet).addMapping(FORWARDED.getPath())) + .addServlet(new ServletInfo("ForwardingServlet", ForwardingServlet).addMapping(FORWARDING_PATH)) + .addServlet(new ServletInfo("IncludingServlet", IncludingServlet).addMapping(INCLUDING_PATH)) .addServlet(new ServletInfo("QueryEncodedBothServlet", QueryEncodedBothServlet).addMapping(QUERY_ENCODED_BOTH.getPath())) .addServlet(new ServletInfo("QueryEncodedServlet", QueryEncodedServlet).addMapping(QUERY_ENCODED_QUERY.getPath())) .addServlet(new ServletInfo("QueryParamServlet", QueryServlet).addMapping(QUERY_PARAM.getPath())) @@ -331,6 +336,34 @@ abstract class UndertowServletTest extends HttpServerTest { body = null } + def "internal #dispatch keeps the original request's resource and servlet path"() { + setup: + def request = new Request.Builder().url(address.resolve(path.substring(1)).toURL()).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() == SUCCESS.status + response.body().string() == SUCCESS.body + + when: + TEST_WRITER.waitForTraces(1) + DDSpan serverSpan = TEST_WRITER.flatten().find { + it.parentId == 0 + } + + then: + serverSpan.resourceName.toString() == (generateHttpRoute() ? "GET $path" : "GET /$CONTEXT$path") + serverSpan.tags["servlet.path"] == path + serverSpan.tags[Tags.HTTP_ROUTE] == (generateHttpRoute() ? path : null) + + where: + dispatch | path + "forward" | FORWARDING_PATH + "include" | INCLUDING_PATH + } + // Pins the block-telemetry-3 wiring added to FormDataParserInstrumentation (urlencoded body, // DoParseAdvice.after) and MultiPartUploadHandlerInstrumentation (multipart body, // ParseBlockingAdvice.after): both now check the boolean returned by diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java index e429c961a11..cc795e69ed2 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java @@ -16,6 +16,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletRequestContext; +import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletRequest; import net.bytebuddy.asm.Advice; @@ -56,7 +57,8 @@ public static class DispatchAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void enter( @Advice.Argument(0) final HttpServerExchange exchange, - @Advice.Argument(1) final ServletRequestContext servletRequestContext) { + @Advice.Argument(1) final ServletRequestContext servletRequestContext, + @Advice.Argument(3) final DispatcherType dispatcherType) { ContextContinuation continuation = exchange.getAttachment(DATADOG_UNDERTOW_CONTINUATION); if (continuation != null) { AgentSpan undertowSpan = spanFromContext(continuation.context()); @@ -65,7 +67,11 @@ public static void enter( undertowSpan.setSpanName(SERVLET_REQUEST); undertowSpan.setTag(SERVLET_CONTEXT, request.getServletContext().getContextPath()); - undertowSpan.setTag(SERVLET_PATH, exchange.getRelativePath()); + // Forward, include, error and async dispatches happen inside the original request, so they + // must not overwrite the original servlet path. + if (dispatcherType == DispatcherType.REQUEST) { + undertowSpan.setTag(SERVLET_PATH, exchange.getRelativePath()); + } } } } diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy new file mode 100644 index 00000000000..90d96d84817 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy @@ -0,0 +1,13 @@ +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class ForwardingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.path).forward(req, resp) + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy new file mode 100644 index 00000000000..b4c7a10b7f4 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy @@ -0,0 +1,13 @@ +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class IncludingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.path).include(req, resp) + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy index 4287b8a8ebe..13281ccb461 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy @@ -1,6 +1,7 @@ import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.base.WebsocketServer +import datadog.trace.core.DDSpan import io.undertow.Handlers import io.undertow.Undertow import io.undertow.UndertowOptions @@ -10,6 +11,7 @@ import io.undertow.servlet.api.ServletContainer import io.undertow.servlet.api.ServletInfo import io.undertow.websockets.jsr.WebSocketDeploymentInfo import jakarta.servlet.MultipartConfigElement +import okhttp3.Request import java.nio.ByteBuffer @@ -30,6 +32,8 @@ import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.USER_B class UndertowServletTest extends HttpServerTest { private static final CONTEXT = "ctx" + private static final String FORWARDING_PATH = "/dispatch/forward" + private static final String INCLUDING_PATH = "/dispatch/include" class UndertowServer implements WebsocketServer { def port = 0 @@ -46,6 +50,8 @@ class UndertowServletTest extends HttpServerTest { .setDeploymentName("servletContext.war") .addServlet(new ServletInfo("SuccessServlet", SuccessServlet).addMapping(SUCCESS.getPath())) .addServlet(new ServletInfo("ForwardedServlet", ForwardedServlet).addMapping(FORWARDED.getPath())) + .addServlet(new ServletInfo("ForwardingServlet", ForwardingServlet).addMapping(FORWARDING_PATH)) + .addServlet(new ServletInfo("IncludingServlet", IncludingServlet).addMapping(INCLUDING_PATH)) .addServlet(new ServletInfo("QueryEncodedBothServlet", QueryEncodedBothServlet).addMapping(QUERY_ENCODED_BOTH.getPath())) .addServlet(new ServletInfo("QueryEncodedServlet", QueryEncodedServlet).addMapping(QUERY_ENCODED_QUERY.getPath())) .addServlet(new ServletInfo("QueryParamServlet", QueryServlet).addMapping(QUERY_PARAM.getPath())) @@ -246,4 +252,32 @@ class UndertowServletTest extends HttpServerTest { throw new UnsupportedOperationException("responseSpan not implemented for " + endpoint) } } + + def "internal #dispatch keeps the original request's resource and servlet path"() { + setup: + def request = new Request.Builder().url(address.resolve(path.substring(1)).toURL()).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.code() == SUCCESS.status + response.body().string() == SUCCESS.body + + when: + TEST_WRITER.waitForTraces(1) + DDSpan serverSpan = TEST_WRITER.flatten().find { + it.parentId == 0 + } + + then: + serverSpan.resourceName.toString() == "GET /$CONTEXT$path" + serverSpan.tags["servlet.path"] == path + + where: + dispatch | path + "forward" | FORWARDING_PATH + "include" | INCLUDING_PATH + } } + From 58890c17920cce380dd543643f855af7caa7608e Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 27 Oct 2026 15:56:01 -0400 Subject: [PATCH 2/3] Add an opt-out to restore Undertow's dispatch renaming DD_TRACE_UNDERTOW_DISPATCH_RENAME_RESOURCE=true brings back the previous behavior where forward, include, error and async dispatches set the route and servlet.path from the dispatch target. It defaults to false, the fixed behavior, since keeping the old naming changes resource names, trace metrics and sampling for anyone relying on it. Co-Authored-By: Claude Opus 5.5 --- .../undertow/ServletInstrumentation.java | 3 ++- .../test/groovy/UndertowServletTest.groovy | 25 ++++++++++++++++--- .../JakartaServletInstrumentation.java | 3 ++- .../test/groovy/UndertowServletTest.groovy | 19 +++++++++++++- .../undertow/UndertowDecorator.java | 2 ++ .../datadog/trace/api/ConfigDefaults.java | 2 ++ .../config/TraceInstrumentationConfig.java | 7 ++++++ .../main/java/datadog/trace/api/Config.java | 12 +++++++++ metadata/supported-configurations.json | 8 ++++++ 9 files changed, 75 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java index 731746b18a0..103ff1acd50 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/ServletInstrumentation.java @@ -77,7 +77,8 @@ public static void enter( undertowSpan.setTag(SERVLET_CONTEXT, request.getServletContext().getContextPath()); // Forward, include, error and async dispatches happen inside the original request, so they // must not rename it: the root span keeps describing what the client asked for. - if (dispatcherType == DispatcherType.REQUEST) { + if (dispatcherType == DispatcherType.REQUEST + || UndertowDecorator.UNDERTOW_DISPATCH_RENAME_RESOURCE) { String relativePath = exchange.getRelativePath(); ServletPathMatch servletPathMatch = servletRequestContext.getServletPathMatch(); diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy index 786c4ec14a6..7eb152f1d45 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy @@ -295,6 +295,10 @@ abstract class UndertowServletTest extends HttpServerTest { true } + def dispatchRenamesResource() { + false + } + @Override Serializable expectedServerSpanRoute(ServerEndpoint endpoint) { if (!generateHttpRoute()) { @@ -354,9 +358,11 @@ abstract class UndertowServletTest extends HttpServerTest { } then: - serverSpan.resourceName.toString() == (generateHttpRoute() ? "GET $path" : "GET /$CONTEXT$path") - serverSpan.tags["servlet.path"] == path - serverSpan.tags[Tags.HTTP_ROUTE] == (generateHttpRoute() ? path : null) + // with the opt-out, a forward renames the request to its target; an include leaves the path alone + def namedPath = dispatchRenamesResource() && dispatch == "forward" ? SUCCESS.path : path + serverSpan.resourceName.toString() == (generateHttpRoute() ? "GET $namedPath" : "GET /$CONTEXT$path") + serverSpan.tags["servlet.path"] == namedPath + serverSpan.tags[Tags.HTTP_ROUTE] == (generateHttpRoute() ? namedPath : null) where: dispatch | path @@ -455,3 +461,16 @@ class UndertowServletNoHttpRouteForkedTest extends UndertowServletTest implement class UndertowServletV1ForkedTest extends UndertowServletTest implements TestingGenericHttpNamingConventions.ServerV1 { } + +class UndertowServletDispatchRenameForkedTest extends UndertowServletTest implements TestingGenericHttpNamingConventions.ServerV0 { + @Override + def dispatchRenamesResource() { + true + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("trace.undertow.dispatch.rename-resource", "true") + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java index cc795e69ed2..d77f50c125e 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/main/java/datadog/trace/instrumentation/undertow/JakartaServletInstrumentation.java @@ -69,7 +69,8 @@ public static void enter( undertowSpan.setTag(SERVLET_CONTEXT, request.getServletContext().getContextPath()); // Forward, include, error and async dispatches happen inside the original request, so they // must not overwrite the original servlet path. - if (dispatcherType == DispatcherType.REQUEST) { + if (dispatcherType == DispatcherType.REQUEST + || UndertowDecorator.UNDERTOW_DISPATCH_RENAME_RESOURCE) { undertowSpan.setTag(SERVLET_PATH, exchange.getRelativePath()); } } diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy index 13281ccb461..56149af201f 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy @@ -253,6 +253,10 @@ class UndertowServletTest extends HttpServerTest { } } + def dispatchRenamesResource() { + false + } + def "internal #dispatch keeps the original request's resource and servlet path"() { setup: def request = new Request.Builder().url(address.resolve(path.substring(1)).toURL()).get().build() @@ -271,8 +275,9 @@ class UndertowServletTest extends HttpServerTest { } then: + // with the opt-out, a forward overwrites servlet.path with its target; an include leaves it alone serverSpan.resourceName.toString() == "GET /$CONTEXT$path" - serverSpan.tags["servlet.path"] == path + serverSpan.tags["servlet.path"] == (dispatchRenamesResource() && dispatch == "forward" ? SUCCESS.path : path) where: dispatch | path @@ -281,3 +286,15 @@ class UndertowServletTest extends HttpServerTest { } } +class UndertowServletDispatchRenameForkedTest extends UndertowServletTest { + @Override + def dispatchRenamesResource() { + true + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("trace.undertow.dispatch.rename-resource", "true") + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-common/src/main/java/datadog/trace/instrumentation/undertow/UndertowDecorator.java b/dd-java-agent/instrumentation/undertow/undertow-common/src/main/java/datadog/trace/instrumentation/undertow/UndertowDecorator.java index e55f76a59c2..d85f55c6343 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-common/src/main/java/datadog/trace/instrumentation/undertow/UndertowDecorator.java +++ b/dd-java-agent/instrumentation/undertow/undertow-common/src/main/java/datadog/trace/instrumentation/undertow/UndertowDecorator.java @@ -43,6 +43,8 @@ public class UndertowDecorator UTF8BytesString.create(DECORATE.operationName()); public static final boolean UNDERTOW_LEGACY_TRACING = Config.get().isLegacyTracingEnabled(true, "undertow"); + public static final boolean UNDERTOW_DISPATCH_RENAME_RESOURCE = + Config.get().isUndertowDispatchRenameResource(); @Override protected String[] instrumentationNames() { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 0035fcc32e3..cc8aa0d2de7 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -99,6 +99,8 @@ public final class ConfigDefaults { static final boolean DEFAULT_CLIENT_IP_ENABLED = false; + static final boolean DEFAULT_UNDERTOW_DISPATCH_RENAME_RESOURCE = false; + static final int DEFAULT_CLOCK_SYNC_PERIOD = 30; // seconds static final boolean DEFAULT_TRACE_LAMBDA_SNAPSTART_CLOCK_RESYNC_ENABLED = false; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java index 137e9805519..6076ff33452 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java @@ -154,6 +154,13 @@ public final class TraceInstrumentationConfig { public static final String SERVLET_PRINCIPAL_ENABLED = "trace.servlet.principal.enabled"; public static final String SERVLET_ASYNC_TIMEOUT_ERROR = "trace.servlet.async-timeout.error"; + /** + * Restores the old Undertow behavior of renaming the request (resource, route and servlet.path) + * to the target of an internal forward, include, error or async dispatch. + */ + public static final String UNDERTOW_DISPATCH_RENAME_RESOURCE = + "trace.undertow.dispatch.rename-resource"; + public static final String SERVLET_ROOT_CONTEXT_SERVICE_NAME = "trace.servlet.root-context.service.name"; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index ee09c1e9e2f..c197db955c4 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -197,6 +197,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_RESOLVER_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_STATS_INTERVAL; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH; +import static datadog.trace.api.ConfigDefaults.DEFAULT_UNDERTOW_DISPATCH_RENAME_RESOURCE; import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_MESSAGES_INHERIT_SAMPLING; import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_MESSAGES_SEPARATE_TRACES; import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_TAG_SESSION_ID; @@ -652,6 +653,7 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_WEBSOCKET_MESSAGES_INHERIT_SAMPLING; import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_WEBSOCKET_MESSAGES_SEPARATE_TRACES; import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_WEBSOCKET_TAG_SESSION_ID; +import static datadog.trace.api.config.TraceInstrumentationConfig.UNDERTOW_DISPATCH_RENAME_RESOURCE; import static datadog.trace.api.config.TracerConfig.AGENT_HOST; import static datadog.trace.api.config.TracerConfig.AGENT_NAMED_PIPE; import static datadog.trace.api.config.TracerConfig.AGENT_PORT_LEGACY; @@ -1340,6 +1342,7 @@ public static String getHostName() { private final boolean servletPrincipalEnabled; private final boolean servletAsyncTimeoutError; + private final boolean undertowDispatchRenameResource; private final boolean springDataRepositoryInterfaceResourceName; @@ -3213,6 +3216,9 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) TRACE_X_DATADOG_TAGS_MAX_LENGTH, DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH); servletAsyncTimeoutError = configProvider.getBoolean(SERVLET_ASYNC_TIMEOUT_ERROR, true); + undertowDispatchRenameResource = + configProvider.getBoolean( + UNDERTOW_DISPATCH_RENAME_RESOURCE, DEFAULT_UNDERTOW_DISPATCH_RENAME_RESOURCE); logLevel = configProvider.getString(TRACE_LOG_LEVEL, null, LOG_LEVEL); debugEnabled = configProvider.getBoolean(TRACE_DEBUG, false); @@ -5222,6 +5228,10 @@ public boolean isServletAsyncTimeoutError() { return servletAsyncTimeoutError; } + public boolean isUndertowDispatchRenameResource() { + return undertowDispatchRenameResource; + } + public ProtocolVersion getProtocolVersion() { return protocolVersion; } @@ -7001,6 +7011,8 @@ public String toString() { + servletPrincipalEnabled + ", servletAsyncTimeoutError=" + servletAsyncTimeoutError + + ", undertowDispatchRenameResource=" + + undertowDispatchRenameResource + ", datadogTagsLimit=" + xDatadogTagsMaxLength + ", traceAgentProtocolVersion=" diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 4b6663c7fe0..c72d475762f 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -11452,6 +11452,14 @@ "aliases": ["DD_TRACE_INTEGRATION_UNDERTOW_2_2_ENABLED", "DD_INTEGRATION_UNDERTOW_2_2_ENABLED"] } ], + "DD_TRACE_UNDERTOW_DISPATCH_RENAME_RESOURCE": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_TRACE_UNDERTOW_ENABLED": [ { "version": "A", From 3cfcc53957eafda60b538ec58259c960866c852d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 27 Oct 2026 16:12:07 -0400 Subject: [PATCH 3/3] Write the dispatching test servlets in Java Co-Authored-By: Claude Opus 5.5 --- .../src/test/groovy/ForwardingServlet.groovy | 13 ------------- .../src/test/groovy/IncludingServlet.groovy | 13 ------------- .../src/test/groovy/UndertowServletTest.groovy | 2 ++ .../undertow/ForwardingServlet.java | 18 ++++++++++++++++++ .../undertow/IncludingServlet.java | 18 ++++++++++++++++++ .../src/test/groovy/ForwardingServlet.groovy | 13 ------------- .../src/test/groovy/IncludingServlet.groovy | 13 ------------- .../src/test/groovy/UndertowServletTest.groovy | 2 ++ .../undertow/ForwardingServlet.java | 18 ++++++++++++++++++ .../undertow/IncludingServlet.java | 18 ++++++++++++++++++ 10 files changed, 76 insertions(+), 52 deletions(-) delete mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy delete mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java delete mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy delete mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java create mode 100644 dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy deleted file mode 100644 index 5a4a3d4063e..00000000000 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/ForwardingServlet.groovy +++ /dev/null @@ -1,13 +0,0 @@ -import javax.servlet.ServletException -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class ForwardingServlet extends HttpServlet { - @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - req.getRequestDispatcher(SUCCESS.path).forward(req, resp) - } -} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy deleted file mode 100644 index 8b5fc514274..00000000000 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/IncludingServlet.groovy +++ /dev/null @@ -1,13 +0,0 @@ -import javax.servlet.ServletException -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class IncludingServlet extends HttpServlet { - @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - req.getRequestDispatcher(SUCCESS.path).include(req, resp) - } -} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy index 7eb152f1d45..c1037796653 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy @@ -5,6 +5,8 @@ import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions import datadog.trace.bootstrap.blocking.BlockingActionHelper import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.core.DDSpan +import datadog.trace.instrumentation.undertow.ForwardingServlet +import datadog.trace.instrumentation.undertow.IncludingServlet import io.undertow.Handlers import io.undertow.Undertow import io.undertow.UndertowOptions diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java new file mode 100644 index 00000000000..67f8a625bb5 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.undertow; + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Forwards to the success servlet through an internal {@code RequestDispatcher}. */ +public class ForwardingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.getPath()).forward(req, resp); + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java new file mode 100644 index 00000000000..2c8ffb4dfd6 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.undertow; + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Includes to the success servlet through an internal {@code RequestDispatcher}. */ +public class IncludingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.getPath()).include(req, resp); + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy deleted file mode 100644 index 90d96d84817..00000000000 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/ForwardingServlet.groovy +++ /dev/null @@ -1,13 +0,0 @@ -import jakarta.servlet.ServletException -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class ForwardingServlet extends HttpServlet { - @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - req.getRequestDispatcher(SUCCESS.path).forward(req, resp) - } -} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy deleted file mode 100644 index b4c7a10b7f4..00000000000 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/IncludingServlet.groovy +++ /dev/null @@ -1,13 +0,0 @@ -import jakarta.servlet.ServletException -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class IncludingServlet extends HttpServlet { - @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - req.getRequestDispatcher(SUCCESS.path).include(req, resp) - } -} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy index 56149af201f..ae58883b2ba 100644 --- a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy @@ -2,6 +2,8 @@ import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.agent.test.base.WebsocketServer import datadog.trace.core.DDSpan +import datadog.trace.instrumentation.undertow.ForwardingServlet +import datadog.trace.instrumentation.undertow.IncludingServlet import io.undertow.Handlers import io.undertow.Undertow import io.undertow.UndertowOptions diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java new file mode 100644 index 00000000000..5632058057a --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/ForwardingServlet.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.undertow; + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** Forwards to the success servlet through an internal {@code RequestDispatcher}. */ +public class ForwardingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.getPath()).forward(req, resp); + } +} diff --git a/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java new file mode 100644 index 00000000000..7d6d09c5938 --- /dev/null +++ b/dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/java/datadog/trace/instrumentation/undertow/IncludingServlet.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.undertow; + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** Includes to the success servlet through an internal {@code RequestDispatcher}. */ +public class IncludingServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher(SUCCESS.getPath()).include(req, resp); + } +}