diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java index a8cb1ebb079..b401e6b4576 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/EndHandlerWrapper.java @@ -1,6 +1,6 @@ package datadog.trace.instrumentation.vertx_3_4.server; -import static datadog.trace.instrumentation.vertx_3_4.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; import static datadog.trace.instrumentation.vertx_3_4.server.VertxDecorator.DECORATE; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java index 6e4c5445d7d..f09054cdee7 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerInstrumentation.java @@ -21,6 +21,7 @@ public RouteHandlerInstrumentation() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", + packageName + ".RouteUpdateHelper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", packageName + ".VertxDecorator$VertxURIDataAdapter", diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java index 1c0d35a0191..359fbb9d5f8 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteHandlerWrapper.java @@ -4,23 +4,20 @@ import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopScope; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; -import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.PARENT_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.updateRoute; import static datadog.trace.instrumentation.vertx_3_4.server.VertxDecorator.DECORATE; import static datadog.trace.instrumentation.vertx_3_4.server.VertxDecorator.INSTRUMENTATION_NAME; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import datadog.trace.bootstrap.instrumentation.api.Tags; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.impl.RouteImpl; import io.vertx.ext.web.impl.RouterImpl; public class RouteHandlerWrapper implements Handler { - static final String PARENT_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".parent"; - static final String HANDLER_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".handler"; - static final String ROUTE_CONTEXT_KEY = "dd." + Tags.HTTP_ROUTE; - private final Handler actual; private final boolean spanStarter; @@ -65,7 +62,8 @@ public void handle(final RoutingContext routingContext) { private void setRoute(RoutingContext routingContext) { final AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY); - if (parentSpan == null) { + final AgentSpan handlerSpan = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); + if (parentSpan == null && handlerSpan == null) { return; } @@ -81,22 +79,6 @@ private void setRoute(RoutingContext routingContext) { } path = mountPoint + path; } - if (method != null && path != null && shouldUpdateRoute(routingContext, parentSpan, path)) { - routingContext.put(ROUTE_CONTEXT_KEY, path); - HTTP_RESOURCE_DECORATOR.withRoute(parentSpan, method, path, true); - } - } - - static boolean shouldUpdateRoute( - final RoutingContext routingContext, final AgentSpan span, final String path) { - if (span == null) { - return false; - } - final String currentRoute = routingContext.get(ROUTE_CONTEXT_KEY); - if (currentRoute != null && currentRoute.equals(path)) { - return false; - } - // do not override route with a "/" if it's already set (it's probably more meaningful) - return !path.equals("/") || span.getTag(Tags.HTTP_ROUTE) == null; + updateRoute(routingContext, method, path, parentSpan, handlerSpan); } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteImplInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteImplInstrumentation.java index 4de2a5d6995..2d8253efd8d 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteImplInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteImplInstrumentation.java @@ -20,8 +20,6 @@ public class RouteImplInstrumentation extends InstrumenterModule implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { - private Advice.PostProcessor.Factory postProcessorFactory; - public RouteImplInstrumentation() { super("vertx", "vertx-3.4"); } @@ -33,11 +31,9 @@ public Reference[] additionalMuzzleReferences() { @Override public boolean isApplicable(Set enabledSystems) { - if (enabledSystems.contains(TargetSystem.IAST)) { - postProcessorFactory = IastPostProcessorFactory.INSTANCE; - return true; - } - return enabledSystems.contains(TargetSystem.APPSEC); + return enabledSystems.contains(TargetSystem.TRACING) + || enabledSystems.contains(TargetSystem.APPSEC) + || enabledSystems.contains(TargetSystem.IAST); } @Override @@ -50,7 +46,7 @@ public String[] knownMatchingTypes() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".PathParameterPublishingHelper", + packageName + ".RouteUpdateHelper", packageName + ".PathParameterPublishingHelper", }; } @@ -82,6 +78,6 @@ public void methodAdvice(MethodTransformer transformer) { @Override public Advice.PostProcessor.Factory postProcessor() { - return postProcessorFactory; + return IastPostProcessorFactory.INSTANCE; } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteMatchesAdvice.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteMatchesAdvice.java index 7aec8ec2487..724bdfc71cf 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteMatchesAdvice.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteMatchesAdvice.java @@ -1,7 +1,12 @@ package datadog.trace.instrumentation.vertx_3_4.server; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.PARENT_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper.updateRoute; + import datadog.trace.api.iast.Source; import datadog.trace.api.iast.SourceTypes; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.vertx.ext.web.RoutingContext; import java.util.Map; import net.bytebuddy.asm.Advice; @@ -16,6 +21,23 @@ static void after( if (ret != 0 || t != null) { return; } + final AgentSpan parentSpan = ctx.get(PARENT_SPAN_CONTEXT_KEY); + final AgentSpan handlerSpan = ctx.get(HANDLER_SPAN_CONTEXT_KEY); + if (ctx.currentRoute() != null) { + final String method = ctx.request().rawMethod(); + String path = ctx.currentRoute().getPath(); + String mountPoint = ctx.mountPoint(); + if (mountPoint != null && !mountPoint.isEmpty()) { + if (mountPoint.charAt(mountPoint.length() - 1) == '/' + && path != null + && !path.isEmpty() + && path.charAt(0) == '/') { + mountPoint = mountPoint.substring(0, mountPoint.length() - 1); + } + path = mountPoint + path; + } + updateRoute(ctx, method, path, parentSpan, handlerSpan); + } Map params = ctx.pathParams(); if (params.isEmpty()) { return; @@ -35,6 +57,23 @@ static void after( if (!ret || t != null) { return; } + final AgentSpan parentSpan = ctx.get(PARENT_SPAN_CONTEXT_KEY); + final AgentSpan handlerSpan = ctx.get(HANDLER_SPAN_CONTEXT_KEY); + if (ctx.currentRoute() != null) { + final String method = ctx.request().rawMethod(); + String path = ctx.currentRoute().getPath(); + String mountPoint = ctx.mountPoint(); + if (mountPoint != null && !mountPoint.isEmpty()) { + if (mountPoint.charAt(mountPoint.length() - 1) == '/' + && path != null + && !path.isEmpty() + && path.charAt(0) == '/') { + mountPoint = mountPoint.substring(0, mountPoint.length() - 1); + } + path = mountPoint + path; + } + updateRoute(ctx, method, path, parentSpan, handlerSpan); + } Map params = ctx.pathParams(); if (params.isEmpty()) { return; diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteUpdateHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteUpdateHelper.java new file mode 100644 index 00000000000..252d743810e --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/main/java/datadog/trace/instrumentation/vertx_3_4/server/RouteUpdateHelper.java @@ -0,0 +1,62 @@ +package datadog.trace.instrumentation.vertx_3_4.server; + +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import io.vertx.ext.web.RoutingContext; + +public final class RouteUpdateHelper { + public static final String PARENT_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".parent"; + public static final String HANDLER_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".handler"; + public static final String ROUTE_CONTEXT_KEY = "dd." + Tags.HTTP_ROUTE; + + private RouteUpdateHelper() {} + + public static void updateRoute( + final RoutingContext routingContext, + final String method, + final String path, + final AgentSpan parentSpan, + final AgentSpan handlerSpan) { + if (method == null || path == null) { + return; + } + if (!shouldUpdateRoute(routingContext, parentSpan, handlerSpan, path)) { + return; + } + + routingContext.put(ROUTE_CONTEXT_KEY, path); + if (parentSpan != null) { + HTTP_RESOURCE_DECORATOR.withRoute(parentSpan, method, path, true); + } + if (handlerSpan != null + && handlerSpan.getResourceNamePriority() < ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE) { + HTTP_RESOURCE_DECORATOR.withRoute(handlerSpan, method, path, true); + } + } + + public static boolean shouldUpdateRoute( + final RoutingContext routingContext, + final AgentSpan parentSpan, + final AgentSpan handlerSpan, + final String path) { + if (parentSpan == null && handlerSpan == null) { + return false; + } + final String currentRoute = routingContext.get(ROUTE_CONTEXT_KEY); + if (currentRoute != null && currentRoute.equals(path)) { + return false; + } + // do not override route with a "/" if it's already set (it's probably more meaningful) + if (!path.equals("/")) { + return true; + } + return !hasHttpRoute(parentSpan) && !hasHttpRoute(handlerSpan); + } + + public static boolean hasHttpRoute(final AgentSpan span) { + return span != null && span.getTag(Tags.HTTP_ROUTE) != null; + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/groovy/server/RouteHandlerWrapperTest.groovy b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/groovy/server/RouteHandlerWrapperTest.groovy new file mode 100644 index 00000000000..135f228e711 --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-3.4/src/test/groovy/server/RouteHandlerWrapperTest.groovy @@ -0,0 +1,47 @@ +package server + +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.vertx_3_4.server.RouteUpdateHelper +import io.vertx.ext.web.RoutingContext + +class RouteHandlerWrapperTest extends InstrumentationSpecification { + + void "updateRoute writes route to both spans"() { + given: + def context = Mock(RoutingContext) + def parentSpan = Mock(AgentSpan) + def handlerSpan = Mock(AgentSpan) + + when: + RouteUpdateHelper.updateRoute( + context, "GET", "/items/:id", parentSpan, handlerSpan) + + then: + 1 * context.get("dd.${Tags.HTTP_ROUTE}") >> null + 1 * context.put("dd.${Tags.HTTP_ROUTE}", "/items/:id") + 1 * parentSpan.setTag(Tags.HTTP_ROUTE, "/items/:id") + 1 * handlerSpan.getResourceNamePriority() >> Byte.MIN_VALUE + 1 * handlerSpan.setTag(Tags.HTTP_ROUTE, "/items/:id") + 0 * _ + } + + void "updateRoute does not replace root route when one exists"() { + given: + def context = Mock(RoutingContext) + def parentSpan = Mock(AgentSpan) + def handlerSpan = Mock(AgentSpan) + + when: + RouteUpdateHelper.updateRoute(context, "GET", "/", parentSpan, handlerSpan) + + then: + 1 * context.get("dd.${Tags.HTTP_ROUTE}") >> null + 1 * parentSpan.getTag(Tags.HTTP_ROUTE) >> "/existing" + 1 * handlerSpan.getTag(Tags.HTTP_ROUTE) >> "/existing" + 0 * context.put(_, _) + 0 * parentSpan.setTag(_, _) + 0 * handlerSpan.setTag(_, _) + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java index a71584b11a7..2db24f42e09 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java @@ -1,6 +1,6 @@ package datadog.trace.instrumentation.vertx_4_0.server; -import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerInstrumentation.java index 5cb72e52e65..081e65546ed 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerInstrumentation.java @@ -27,6 +27,7 @@ public Reference[] additionalMuzzleReferences() { public String[] helperClassNames() { return new String[] { packageName + ".EndHandlerWrapper", + packageName + ".RouteUpdateHelper", packageName + ".RouteHandlerWrapper", packageName + ".VertxDecorator", }; diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java index a996806a1a8..2a521d87c7a 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java @@ -4,22 +4,19 @@ import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopScope; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; -import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.PARENT_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.updateRoute; import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE; import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.INSTRUMENTATION_NAME; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import datadog.trace.bootstrap.instrumentation.api.Tags; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.impl.RouteImpl; public class RouteHandlerWrapper implements Handler { - static final String PARENT_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".parent"; - static final String HANDLER_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".handler"; - static final String ROUTE_CONTEXT_KEY = "dd." + Tags.HTTP_ROUTE; - private final Handler actual; private final boolean spanStarter; @@ -62,7 +59,8 @@ public void handle(final RoutingContext routingContext) { private void setRoute(RoutingContext routingContext) { final AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY); - if (parentSpan == null) { + final AgentSpan handlerSpan = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); + if (parentSpan == null && handlerSpan == null) { return; } @@ -82,22 +80,6 @@ private void setRoute(RoutingContext routingContext) { : mountPoint; path = noBackslashhMountPoint + path; } - if (method != null && path != null && shouldUpdateRoute(routingContext, parentSpan, path)) { - routingContext.put(ROUTE_CONTEXT_KEY, path); - HTTP_RESOURCE_DECORATOR.withRoute(parentSpan, method, path, true); - } - } - - static boolean shouldUpdateRoute( - final RoutingContext routingContext, final AgentSpan span, final String path) { - if (span == null) { - return false; - } - final String currentRoute = routingContext.get(ROUTE_CONTEXT_KEY); - if (currentRoute != null && currentRoute.equals(path)) { - return false; - } - // do not override route with a "/" if it's already set (it's probably more meaningful) - return !path.equals("/") || span.getTag(Tags.HTTP_ROUTE) == null; + updateRoute(routingContext, method, path, parentSpan, handlerSpan); } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteImplInstrumentation.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteImplInstrumentation.java index a2041f43264..944ae4b7196 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteImplInstrumentation.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteImplInstrumentation.java @@ -18,8 +18,6 @@ public class RouteImplInstrumentation extends InstrumenterModule implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { - private Advice.PostProcessor.Factory postProcessorFactory; - public RouteImplInstrumentation() { super("vertx", "vertx-4.0"); } @@ -31,11 +29,9 @@ public Reference[] additionalMuzzleReferences() { @Override public boolean isApplicable(Set enabledSystems) { - if (enabledSystems.contains(TargetSystem.IAST)) { - postProcessorFactory = IastPostProcessorFactory.INSTANCE; - return true; - } - return enabledSystems.contains(TargetSystem.APPSEC); + return enabledSystems.contains(TargetSystem.TRACING) + || enabledSystems.contains(TargetSystem.APPSEC) + || enabledSystems.contains(TargetSystem.IAST); } @Override @@ -48,7 +44,7 @@ public String[] knownMatchingTypes() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".PathParameterPublishingHelper", + packageName + ".RouteUpdateHelper", packageName + ".PathParameterPublishingHelper", }; } @@ -80,6 +76,6 @@ public void methodAdvice(MethodTransformer transformer) { @Override public Advice.PostProcessor.Factory postProcessor() { - return postProcessorFactory; + return IastPostProcessorFactory.INSTANCE; } } diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteMatchesAdvice.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteMatchesAdvice.java index f52ed53ece3..1a48e8339d3 100644 --- a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteMatchesAdvice.java +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteMatchesAdvice.java @@ -1,7 +1,12 @@ package datadog.trace.instrumentation.vertx_4_0.server; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.HANDLER_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.PARENT_SPAN_CONTEXT_KEY; +import static datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper.updateRoute; + import datadog.trace.api.iast.Source; import datadog.trace.api.iast.SourceTypes; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import io.vertx.ext.web.RoutingContext; import java.util.Map; import net.bytebuddy.asm.Advice; @@ -16,6 +21,24 @@ static void after( if (ret != 0 || t != null) { return; } + final AgentSpan parentSpan = ctx.get(PARENT_SPAN_CONTEXT_KEY); + final AgentSpan handlerSpan = ctx.get(HANDLER_SPAN_CONTEXT_KEY); + if (ctx.currentRoute() != null) { + final String method = ctx.request().method().name(); + String path = ctx.currentRoute().getPath(); + if (path == null) { + path = ctx.currentRoute().getName(); + } + final String mountPoint = ctx.mountPoint(); + if (mountPoint != null && path != null) { + final String noBackslashhMountPoint = + mountPoint.endsWith("/") + ? mountPoint.substring(0, mountPoint.lastIndexOf("/")) + : mountPoint; + path = noBackslashhMountPoint + path; + } + updateRoute(ctx, method, path, parentSpan, handlerSpan); + } Map params = ctx.pathParams(); if (params.isEmpty()) { return; @@ -39,6 +62,24 @@ static void after( if (!ret || t != null) { return; } + final AgentSpan parentSpan = ctx.get(PARENT_SPAN_CONTEXT_KEY); + final AgentSpan handlerSpan = ctx.get(HANDLER_SPAN_CONTEXT_KEY); + if (ctx.currentRoute() != null) { + final String method = ctx.request().method().name(); + String path = ctx.currentRoute().getPath(); + if (path == null) { + path = ctx.currentRoute().getName(); + } + final String mountPoint = ctx.mountPoint(); + if (mountPoint != null && path != null) { + final String noBackslashhMountPoint = + mountPoint.endsWith("/") + ? mountPoint.substring(0, mountPoint.lastIndexOf("/")) + : mountPoint; + path = noBackslashhMountPoint + path; + } + updateRoute(ctx, method, path, parentSpan, handlerSpan); + } Map params = ctx.pathParams(); if (params.isEmpty()) { return; diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteUpdateHelper.java b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteUpdateHelper.java new file mode 100644 index 00000000000..1e7de06a70f --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteUpdateHelper.java @@ -0,0 +1,62 @@ +package datadog.trace.instrumentation.vertx_4_0.server; + +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import io.vertx.ext.web.RoutingContext; + +public final class RouteUpdateHelper { + public static final String PARENT_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".parent"; + public static final String HANDLER_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".handler"; + public static final String ROUTE_CONTEXT_KEY = "dd." + Tags.HTTP_ROUTE; + + private RouteUpdateHelper() {} + + public static void updateRoute( + final RoutingContext routingContext, + final String method, + final String path, + final AgentSpan parentSpan, + final AgentSpan handlerSpan) { + if (method == null || path == null) { + return; + } + if (!shouldUpdateRoute(routingContext, parentSpan, handlerSpan, path)) { + return; + } + + routingContext.put(ROUTE_CONTEXT_KEY, path); + if (parentSpan != null) { + HTTP_RESOURCE_DECORATOR.withRoute(parentSpan, method, path, true); + } + if (handlerSpan != null + && handlerSpan.getResourceNamePriority() < ResourceNamePriorities.HTTP_FRAMEWORK_ROUTE) { + HTTP_RESOURCE_DECORATOR.withRoute(handlerSpan, method, path, true); + } + } + + public static boolean shouldUpdateRoute( + final RoutingContext routingContext, + final AgentSpan parentSpan, + final AgentSpan handlerSpan, + final String path) { + if (parentSpan == null && handlerSpan == null) { + return false; + } + final String currentRoute = routingContext.get(ROUTE_CONTEXT_KEY); + if (currentRoute != null && currentRoute.equals(path)) { + return false; + } + // do not override route with a "/" if it's already set (it's probably more meaningful) + if (!path.equals("/")) { + return true; + } + return !hasHttpRoute(parentSpan) && !hasHttpRoute(handlerSpan); + } + + public static boolean hasHttpRoute(final AgentSpan span) { + return span != null && span.getTag(Tags.HTTP_ROUTE) != null; + } +} diff --git a/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/RouteHandlerWrapperTest.groovy b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/RouteHandlerWrapperTest.groovy new file mode 100644 index 00000000000..7c1126eacdd --- /dev/null +++ b/dd-java-agent/instrumentation/vertx/vertx-web/vertx-web-4.0/src/test/groovy/server/RouteHandlerWrapperTest.groovy @@ -0,0 +1,47 @@ +package server + +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.vertx_4_0.server.RouteUpdateHelper +import io.vertx.ext.web.RoutingContext + +class RouteHandlerWrapperTest extends InstrumentationSpecification { + + void "updateRoute writes route to both spans"() { + given: + def context = Mock(RoutingContext) + def parentSpan = Mock(AgentSpan) + def handlerSpan = Mock(AgentSpan) + + when: + RouteUpdateHelper.updateRoute( + context, "GET", "/items/:id", parentSpan, handlerSpan) + + then: + 1 * context.get("dd.${Tags.HTTP_ROUTE}") >> null + 1 * context.put("dd.${Tags.HTTP_ROUTE}", "/items/:id") + 1 * parentSpan.setTag(Tags.HTTP_ROUTE, "/items/:id") + 1 * handlerSpan.getResourceNamePriority() >> Byte.MIN_VALUE + 1 * handlerSpan.setTag(Tags.HTTP_ROUTE, "/items/:id") + 0 * _ + } + + void "updateRoute does not replace root route when one exists"() { + given: + def context = Mock(RoutingContext) + def parentSpan = Mock(AgentSpan) + def handlerSpan = Mock(AgentSpan) + + when: + RouteUpdateHelper.updateRoute(context, "GET", "/", parentSpan, handlerSpan) + + then: + 1 * context.get("dd.${Tags.HTTP_ROUTE}") >> null + 1 * parentSpan.getTag(Tags.HTTP_ROUTE) >> "/existing" + 1 * handlerSpan.getTag(Tags.HTTP_ROUTE) >> "/existing" + 0 * context.put(_, _) + 0 * parentSpan.setTag(_, _) + 0 * handlerSpan.setTag(_, _) + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/.gitignore b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/.gitignore new file mode 100644 index 00000000000..1ec2c548b11 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/.gitignore @@ -0,0 +1,6 @@ +# Ignore all project specific gradle directories/files +.gradle +gradle +build +gradlew +gradlew.bat diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/build.gradle b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/build.gradle new file mode 100644 index 00000000000..267a61d3500 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.5' + id 'io.spring.dependency-management' version '1.1.7' +} + +def sharedRootDir = "$rootDir/../../../" +def sharedConfigDirectory = "$sharedRootDir/gradle" +rootProject.ext.sharedConfigDirectory = sharedConfigDirectory + +apply from: "$sharedConfigDirectory/repositories.gradle" + +if (hasProperty('appBuildDir')) { + buildDir = property('appBuildDir') +} + +version = "" + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-undertow' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation group: 'com.h2database', name: 'h2', version: '2.3.232' + compileOnly group:"com.google.code.findbugs", name:"jsr305", version:"3.0.2" + + if (hasProperty('apiJar')) { + implementation files(property('apiJar')) + } else { + implementation "com.datadoghq:dd-trace-api:1.2.0" + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/settings.gradle b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/settings.gradle new file mode 100644 index 00000000000..fad11f4c4a5 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/settings.gradle @@ -0,0 +1,34 @@ +pluginManagement { + repositories { + mavenLocal() + if (settings.hasProperty("gradlePluginProxy")) { + maven { + url settings["gradlePluginProxy"] + allowInsecureProtocol = true + } + } + if (settings.hasProperty("mavenRepositoryProxy")) { + maven { + url settings["mavenRepositoryProxy"] + allowInsecureProtocol = true + } + } + gradlePluginPortal() + mavenCentral() + } +} + +def isCI = providers.environmentVariable("CI").isPresent() + +// Don't pollute the dependency cache with the build cache +if (isCI) { + def sharedRootDir = "$rootDir/../../../" + buildCache { + local { + // This needs to line up with the code in the outer project settings.gradle + directory = "$sharedRootDir/workspace/build-cache" + } + } +} + +rootProject.name='webmvc-3.5-undertow-smoketest' diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/AppInitializer.java b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/AppInitializer.java new file mode 100644 index 00000000000..a734f5bcd19 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/AppInitializer.java @@ -0,0 +1,22 @@ +package datadog.smoketest.springboot; + +import datadog.smoketest.springboot.model.Fruit; +import datadog.smoketest.springboot.repository.FruitRepository; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +@Service +public class AppInitializer implements InitializingBean { + private final FruitRepository fruitRepository; + + public AppInitializer(FruitRepository fruitRepository) { + this.fruitRepository = fruitRepository; + } + + @Override + public void afterPropertiesSet() throws Exception { + fruitRepository.save(new Fruit("apple")); + fruitRepository.save(new Fruit("banana")); + fruitRepository.save(new Fruit("orange")); + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java new file mode 100644 index 00000000000..e26a5350e0e --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java @@ -0,0 +1,14 @@ +package datadog.smoketest.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@EnableJpaRepositories +public class SpringbootApplication { + + public static void main(final String[] args) { + SpringApplication.run(SpringbootApplication.class, args); + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/controller/FruitController.java b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/controller/FruitController.java new file mode 100644 index 00000000000..9818e3ef87d --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/controller/FruitController.java @@ -0,0 +1,33 @@ +package datadog.smoketest.springboot.controller; + +import datadog.smoketest.springboot.model.Fruit; +import datadog.smoketest.springboot.repository.FruitRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/fruits") +public class FruitController { + + private final FruitRepository fruitRepository; + + public FruitController(FruitRepository fruitRepository) { + this.fruitRepository = fruitRepository; + } + + @GetMapping + public Iterable listFruits() { + return fruitRepository.findAll(); + } + + @GetMapping("/{name}") + public ResponseEntity findOneFruit(@PathVariable("name") final String name) { + return fruitRepository + .findByName(name) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/model/Fruit.java b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/model/Fruit.java new file mode 100644 index 00000000000..32cae6ce653 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/model/Fruit.java @@ -0,0 +1,58 @@ +package datadog.smoketest.springboot.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Objects; +import javax.annotation.Nonnull; + +@Entity +@Table +public class Fruit { + + public Fruit() {} + + public Fruit(@Nonnull String name) { + this.name = name; + } + + @Id @GeneratedValue private Long id; + + @Column(nullable = false) + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Fruit fruit = (Fruit) o; + return Objects.equals(id, fruit.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/repository/FruitRepository.java b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/repository/FruitRepository.java new file mode 100644 index 00000000000..0ae8142a77b --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/java/datadog/smoketest/springboot/repository/FruitRepository.java @@ -0,0 +1,12 @@ +package datadog.smoketest.springboot.repository; + +import datadog.smoketest.springboot.model.Fruit; +import java.util.Optional; +import javax.annotation.Nonnull; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FruitRepository extends CrudRepository { + Optional findByName(@Nonnull final String name); +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/resources/application.properties b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/resources/application.properties new file mode 100644 index 00000000000..84891dd7426 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/application/src/main/resources/application.properties @@ -0,0 +1,16 @@ +logging.level.root=INFO +spring.datasource.url=jdbc:h2:mem:fruitdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.generate-ddl=true +spring.main.web-application-type=servlet +management.endpoints.web.exposure.include=health,info +logging.level.root=INFO +spring.datasource.url=jdbc:h2:mem:fruitdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.generate-ddl=true diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/build.gradle b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/build.gradle new file mode 100644 index 00000000000..3e3a0646a33 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/build.gradle @@ -0,0 +1,121 @@ +apply from: "$rootDir/gradle/java.gradle" +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_21 +} + +description = 'Spring Boot 3.5 WebMvc Undertow Smoke Tests.' + +dependencies { + testImplementation project(':dd-smoke-tests') +} + +def appDir = "$projectDir/application" +def appBuildDir = "$buildDir/application" +def isWindows = System.getProperty("os.name").toLowerCase().contains("win") +def gradlewCommand = isWindows ? 'gradlew.bat' : 'gradlew' + +tasks.register('webmvcUndertowBuild35', Exec) { + workingDir "$appDir" + environment += [ + "GRADLE_OPTS": "-Dorg.gradle.jvmargs='-Xmx512M'", + "JAVA_HOME": getLazyJavaHomeFor(21) + ] + commandLine "$rootDir/${gradlewCommand}", "bootJar", "--no-daemon", "--max-workers=4", "-PappBuildDir=$appBuildDir", "-PapiJar=${project(':dd-trace-api').tasks.jar.archiveFile.get()}" + + outputs.cacheIf { true } + + outputs.dir(appBuildDir) + .withPropertyName("applicationJar") + + inputs.files(fileTree(appDir) { + include '**/*' + exclude '.gradle/**' + }) + .withPropertyName("application") + .withPathSensitivity(PathSensitivity.RELATIVE) +} + +tasks.named("compileTestGroovy", GroovyCompile) { + dependsOn project(':dd-trace-api').tasks.named("jar") + dependsOn 'webmvcUndertowBuild35' + outputs.upToDateWhen { + !webmvcUndertowBuild35.didWork + } +} + +tasks.withType(Test).configureEach { + jvmArgs "-Ddatadog.smoketest.springboot.uberJar.path=$appBuildDir/libs/webmvc-3.5-undertow-smoketest.jar" +} + +def releasedAgentsDir = file("$buildDir/released-agents") +def agent1604File = file("$releasedAgentsDir/dd-java-agent-1.60.4.jar") +def agent1610File = file("$releasedAgentsDir/dd-java-agent-1.61.0.jar") + +def registerDownloadTask = { String taskName, String url, File outputFile -> + tasks.register(taskName) { + description = "Download ${outputFile.name} for regression checks." + outputs.file(outputFile) + doLast { + outputFile.parentFile.mkdirs() + if (!outputFile.exists()) { + new URL(url).withInputStream { inStream -> + Files.copy(inStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + } + } +} + +def downloadAgent1604 = registerDownloadTask( + "downloadAgent1604", + "https://github.com/DataDog/dd-trace-java/releases/download/v1.60.4/dd-java-agent.jar", + agent1604File + ) + +def downloadAgent1610 = registerDownloadTask( + "downloadAgent1610", + "https://github.com/DataDog/dd-trace-java/releases/download/v1.61.0/dd-java-agent.jar", + agent1610File + ) + +def configureReleasedAgentTestTask = { String taskName, File agentJar, TaskProvider downloadTask -> + tasks.register(taskName, Test) { + group = "verification" + description = "Run Spring Boot 3.5 Undertow smoke test with ${agentJar.name}" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + dependsOn tasks.named("testClasses") + dependsOn "webmvcUndertowBuild35" + dependsOn downloadTask + useJUnitPlatform() + filter { + includeTestsMatching("SpringBoot35UndertowIntegrationTest") + } + // Override the default local-agent path injected by :dd-smoke-tests. + systemProperty "datadog.smoketest.agent.jar.path.override", agentJar.absolutePath + jvmArgs "-Ddatadog.smoketest.springboot.uberJar.path=$appBuildDir/libs/webmvc-3.5-undertow-smoketest.jar" + } +} + +def testWithAgent1604 = configureReleasedAgentTestTask("testWithAgent1604", agent1604File, downloadAgent1604) +def testWithAgent1610 = configureReleasedAgentTestTask("testWithAgent1610", agent1610File, downloadAgent1610) + +tasks.register("reproducerMatrix") { + group = "verification" + description = "Run reproducer against 1.60.4 then 1.61.0." + dependsOn testWithAgent1604 + dependsOn testWithAgent1610 +} + +spotless { + java { + target "**/*.java" + } + + groovyGradle { + target '*.gradle', "**/*.gradle" + } +} diff --git a/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/src/test/groovy/SpringBoot35UndertowIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/src/test/groovy/SpringBoot35UndertowIntegrationTest.groovy new file mode 100644 index 00000000000..3bf387bb53f --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.5-webmvc-undertow/src/test/groovy/SpringBoot35UndertowIntegrationTest.groovy @@ -0,0 +1,105 @@ +import datadog.smoketest.AbstractServerSmokeTest +import okhttp3.Request + +import java.util.concurrent.atomic.AtomicInteger +import java.util.regex.Pattern + +class SpringBoot35UndertowIntegrationTest extends AbstractServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.uberJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll((String[]) [ + "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", + "-Ddd.trace.debug=true", + "-jar", + springBootShadowJar, + "--server.port=${httpPort}" + ]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + @Override + File createTemporaryFile() { + return File.createTempFile("trace-structure-docs", "out") + } + + @Override + protected Set expectedTraces() { + return [ + "\\[servlet\\.request:GET /fruits\\[spring\\.handler:FruitController\\.listFruits\\[repository\\.operation:FruitRepository\\.findAll\\[h2\\.query:.*", + "\\[servlet\\.request:GET /fruits/\\{name}\\[spring\\.handler:FruitController\\.findOneFruit\\[repository\\.operation:FruitRepository\\.findByName\\[h2\\.query:.*" + ] + } + + @Override + protected Set assertTraceCounts(Set expected, Map traceCounts) { + List remaining = expected.collect { Pattern.compile(it) }.toList() + for (def i = remaining.size() - 1; i >= 0; i--) { + for (Map.Entry entry : traceCounts.entrySet()) { + if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { + remaining.remove(i) + break + } + } + } + return remaining.collect { it.pattern() }.toSet() + } + + def "responds to /fruits without hanging"() { + setup: + String url = "http://localhost:${httpPort}/fruits" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + def responseBodyStr = response.body().string() + responseBodyStr != null + ["banana", "apple", "orange"].each { responseBodyStr.contains(it) } + waitForTraceCount(1) + } + + def "responds to /fruits/{name} without hanging"() { + setup: + String url = "http://localhost:${httpPort}/fruits/banana" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + def responseBodyStr = response.body().string() + responseBodyStr != null + responseBodyStr.contains("banana") + waitForTraceCount(1) + } + + def "actuator health endpoint remains responsive"() { + setup: + String url = "http://localhost:${httpPort}/actuator/health" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + response.body().string().contains("UP") + } + + @Override + boolean testTelemetry() { + false + } + + @Override + List expectedTelemetryDependencies() { + ['spring-core', 'io.undertow:undertow-core'] + } +} diff --git a/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy b/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy index 31e24c70488..891c5c04d02 100644 --- a/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy +++ b/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy @@ -19,7 +19,10 @@ abstract class ProcessManager extends Specification { @Shared protected String buildDirectory = System.getProperty("datadog.smoketest.builddir") @Shared - protected String shadowJarPath = System.getProperty("datadog.smoketest.agent.shadowJar.path") + protected String shadowJarPath = + System.getProperty( + "datadog.smoketest.agent.jar.path.override", + System.getProperty("datadog.smoketest.agent.shadowJar.path")) @Shared protected boolean isIBM = System.getProperty("java.vendor", "").contains("IBM") diff --git a/settings.gradle.kts b/settings.gradle.kts index 074514e7126..0aad88cfca7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -228,6 +228,7 @@ include( ":dd-smoke-tests:spring-boot-2.6-webmvc", ":dd-smoke-tests:spring-boot-3.0-webmvc", ":dd-smoke-tests:spring-boot-3.3-webmvc", + ":dd-smoke-tests:spring-boot-3.5-webmvc-undertow", ":dd-smoke-tests:spring-boot-rabbit", ":dd-smoke-tests:spring-security", ":dd-smoke-tests:springboot",