diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceDecorator.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceDecorator.java new file mode 100644 index 0000000000..baee43cde0 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceDecorator.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.jdbc; + +import datadog.trace.agent.decorator.BaseDecorator; + +public class DataSourceDecorator extends BaseDecorator { + public static final DataSourceDecorator DECORATE = new DataSourceDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"jdbc-datasource"}; + } + + @Override + protected String component() { + return "java-jdbc-connection"; + } + + @Override + protected String spanType() { + return null; + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceInstrumentation.java new file mode 100644 index 0000000000..e5930375c8 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceInstrumentation.java @@ -0,0 +1,81 @@ +package datadog.trace.instrumentation.jdbc; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jdbc.DataSourceDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.DDTags; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.sql.DataSource; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class DataSourceInstrumentation extends Instrumenter.Default { + public DataSourceInstrumentation() { + super("jdbc-datasource"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", packageName + ".DataSourceDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.sql.DataSource"))); + } + + @Override + public Map, String> transformers() { + return singletonMap(named("getConnection"), GetConnectionAdvice.class.getName()); + } + + public static class GetConnectionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.This final DataSource ds) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("database.connection"); + DECORATE.afterStart(span); + + span.setTag(DDTags.RESOURCE_NAME, ds.getClass().getSimpleName() + ".getConnection"); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy index 54b5bee265..c1d2a9fc25 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy @@ -5,8 +5,10 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.instrumentation.api.Tags +import org.apache.derby.jdbc.EmbeddedDataSource import org.apache.derby.jdbc.EmbeddedDriver import org.h2.Driver +import org.h2.jdbcx.JdbcDataSource import org.hsqldb.jdbc.JDBCDriver import spock.lang.Shared import spock.lang.Unroll @@ -25,6 +27,9 @@ import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace class JDBCInstrumentationTest extends AgentTestRunner { + static { + System.setProperty("dd.integration.jdbc-datasource.enabled", "true") + } @Shared def dbName = "jdbcUnitTest" @@ -550,6 +555,66 @@ class JDBCInstrumentationTest extends AgentTestRunner { false | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" } + def "calling #datasource.class.simpleName getConnection generates a span when under existing trace"() { + setup: + assert datasource instanceof DataSource + init?.call(datasource) + + when: + datasource.getConnection().close() + + then: + !TEST_WRITER.any { it.any { it.operationName == "database.connection" } } + TEST_WRITER.clear() + + when: + runUnderTrace("parent") { + datasource.getConnection().close() + } + + then: + assertTraces(1) { + trace(0, recursive ? 3 : 2) { + basicSpan(it, 0, "parent") + + span(1) { + operationName "database.connection" + resourceName "${datasource.class.simpleName}.getConnection" + childOf span(0) + tags { + "$Tags.COMPONENT" "java-jdbc-connection" + defaultTags() + } + } + if (recursive) { + span(2) { + operationName "database.connection" + resourceName "${datasource.class.simpleName}.getConnection" + childOf span(1) + tags { + "$Tags.COMPONENT" "java-jdbc-connection" + defaultTags() + } + } + } + } + } + + where: + datasource | init + new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) } + new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") } + cpDatasources.get("hikari").get("h2") | null + cpDatasources.get("hikari").get("derby") | null + cpDatasources.get("c3p0").get("h2") | null + cpDatasources.get("c3p0").get("derby") | null + + // Tomcat's pool doesn't work because the getConnection method is + // implemented in a parent class that doesn't implement DataSource + + recursive = datasource instanceof EmbeddedDataSource + } + def "test getClientInfo exception"() { setup: Connection connection = new TestConnection(false) diff --git a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Advice.java b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Advice.java index 87735d6a17..2264d9945b 100644 --- a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Advice.java +++ b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Advice.java @@ -9,6 +9,7 @@ import static datadog.trace.instrumentation.servlet2.HttpServletRequestExtractAd import static datadog.trace.instrumentation.servlet2.Servlet2Decorator.DECORATE; import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.api.AgentScope; import datadog.trace.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.api.Tags; @@ -36,12 +37,16 @@ public class Servlet2Advice { return null; } + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + if (response instanceof HttpServletResponse) { + // For use by HttpServletResponseInstrumentation: + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class) + .put((HttpServletResponse) response, httpServletRequest); + response = new StatusSavingHttpServletResponseWrapper((HttpServletResponse) response); } - final HttpServletRequest httpServletRequest = (HttpServletRequest) request; - final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER); final AgentSpan span = diff --git a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Instrumentation.java b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Instrumentation.java index 5f309738d4..8490d7a237 100644 --- a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Instrumentation.java +++ b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Instrumentation.java @@ -49,6 +49,12 @@ public final class Servlet2Instrumentation extends Instrumenter.Default { named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet")))); } + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + /** * Here we are instrumenting the public method for HttpServlet. This should ensure that this * advice is always called before HttpServletInstrumentation which is instrumenting the protected diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/HttpServletRequestExtractAdapter.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/HttpServletRequestExtractAdapter.java index e8b1fff393..3b904d12ff 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/HttpServletRequestExtractAdapter.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/HttpServletRequestExtractAdapter.java @@ -1,7 +1,6 @@ package datadog.trace.instrumentation.servlet3; import datadog.trace.instrumentation.api.AgentPropagation; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -14,7 +13,7 @@ public class HttpServletRequestExtractAdapter @Override public List keys(final HttpServletRequest carrier) { - final ArrayList keys = Collections.list(carrier.getHeaderNames()); + final List keys = Collections.list(carrier.getHeaderNames()); keys.addAll(Collections.list(carrier.getAttributeNames())); return keys; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java index dbc1a4419d..407520288a 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java @@ -9,6 +9,7 @@ import static datadog.trace.instrumentation.servlet3.HttpServletRequestExtractAd import static datadog.trace.instrumentation.servlet3.Servlet3Decorator.DECORATE; import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.api.AgentScope; import datadog.trace.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.api.Tags; @@ -24,7 +25,10 @@ public class Servlet3Advice { @Advice.OnMethodEnter(suppress = Throwable.class) public static AgentScope onEnter( - @Advice.This final Object servlet, @Advice.Argument(0) final ServletRequest request) { + @Advice.This final Object servlet, + @Advice.Argument(0) final ServletRequest request, + @Advice.Argument(1) final ServletResponse response) { + final boolean hasActiveTrace = activeSpan() != null; final boolean hasServletTrace = request.getAttribute(DD_SPAN_ATTRIBUTE) instanceof AgentSpan; final boolean invalidRequest = !(request instanceof HttpServletRequest); @@ -35,6 +39,10 @@ public class Servlet3Advice { final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + // For use by HttpServletResponseInstrumentation: + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class) + .put((HttpServletResponse) response, httpServletRequest); + final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER); final AgentSpan span = diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java index 0fb9455ac6..42c982b169 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java @@ -41,6 +41,12 @@ public final class Servlet3Instrumentation extends Instrumenter.Default { named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet")))); } + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + /** * Here we are instrumenting the public method for HttpServlet. This should ensure that this * advice is always called before HttpServletInstrumentation which is instrumenting the protected diff --git a/dd-java-agent/instrumentation/servlet/servlet.gradle b/dd-java-agent/instrumentation/servlet/servlet.gradle index ff6901ae6f..50f9d1e4b1 100644 --- a/dd-java-agent/instrumentation/servlet/servlet.gradle +++ b/dd-java-agent/instrumentation/servlet/servlet.gradle @@ -1 +1,29 @@ +muzzle { + pass { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[,]" + assertInverse = true + } + pass { + group = "javax.servlet" + module = 'servlet-api' + versions = "[,]" + } +} + apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3' + + testCompile group: 'javax.servlet', name: 'servlet-api', version: '2.3' + + // servlet request instrumentation required for linking request to response. + testCompile project(':dd-java-agent:instrumentation:servlet:request-2') + + // Don't want to conflict with jetty from the test server. + testCompile(project(':dd-java-agent:testing')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/ServletRequestSetter.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/ServletRequestSetter.java new file mode 100644 index 0000000000..8719726664 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/ServletRequestSetter.java @@ -0,0 +1,14 @@ +package datadog.trace.instrumentation.servlet; + +import datadog.trace.instrumentation.api.AgentPropagation; +import javax.servlet.ServletRequest; + +/** Inject into request attributes since the request headers can't be modified. */ +public class ServletRequestSetter implements AgentPropagation.Setter { + public static final ServletRequestSetter SETTER = new ServletRequestSetter(); + + @Override + public void set(final ServletRequest carrier, final String key, final String value) { + carrier.setAttribute(key, value); + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java new file mode 100644 index 0000000000..b20f3c19e7 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.servlet.dispatcher; + +import datadog.trace.agent.decorator.BaseDecorator; + +public class RequestDispatcherDecorator extends BaseDecorator { + public static final RequestDispatcherDecorator DECORATE = new RequestDispatcherDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet", "servlet-dispatcher"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-dispatcher"; + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java new file mode 100644 index 0000000000..86ddbd3c69 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java @@ -0,0 +1,113 @@ +package datadog.trace.instrumentation.servlet.dispatcher; + +import static datadog.trace.agent.decorator.HttpServerDecorator.DD_SPAN_ATTRIBUTE; +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.servlet.ServletRequestSetter.SETTER; +import static datadog.trace.instrumentation.servlet.dispatcher.RequestDispatcherDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class RequestDispatcherInstrumentation extends Instrumenter.Default { + public RequestDispatcherInstrumentation() { + super("servlet", "servlet-dispatcher"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.instrumentation.servlet.ServletRequestSetter", + "datadog.trace.agent.decorator.BaseDecorator", + packageName + ".RequestDispatcherDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.RequestDispatcher"))); + } + + @Override + public Map contextStore() { + return singletonMap("javax.servlet.RequestDispatcher", String.class.getName()); + } + + @Override + public Map, String> transformers() { + return singletonMap( + named("forward") + .or(named("include")) + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + RequestDispatcherAdvice.class.getName()); + } + + public static class RequestDispatcherAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start( + @Advice.Origin("#m") final String method, + @Advice.This final RequestDispatcher dispatcher, + @Advice.Argument(0) final ServletRequest request) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet." + method); + DECORATE.afterStart(span); + + final String target = + InstrumentationContext.get(RequestDispatcher.class, String.class).get(dispatcher); + span.setTag(DDTags.RESOURCE_NAME, target); + + // In case we lose context, inject trace into to the request. + propagate().inject(span, request, SETTER); + + // temporarily remove from request to avoid spring resource name bubbling up: + request.removeAttribute(DD_SPAN_ATTRIBUTE); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stop( + @Advice.Enter final AgentScope scope, + @Advice.Argument(0) final ServletRequest request, + @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + + // now add it back... + request.setAttribute(DD_SPAN_ATTRIBUTE, scope.span()); + + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java new file mode 100644 index 0000000000..9c3df78076 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java @@ -0,0 +1,57 @@ +package datadog.trace.instrumentation.servlet.dispatcher; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.InstrumentationContext; +import java.util.Map; +import javax.servlet.RequestDispatcher; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class ServletContextInstrumentation extends Instrumenter.Default { + public ServletContextInstrumentation() { + super("servlet", "servlet-dispatcher"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.ServletContext"))); + } + + @Override + public Map contextStore() { + return singletonMap("javax.servlet.RequestDispatcher", String.class.getName()); + } + + @Override + public Map, String> transformers() { + return singletonMap( + returns(named("javax.servlet.RequestDispatcher")) + .and(takesArgument(0, String.class)) + // javax.servlet.ServletContext.getRequestDispatcher + // javax.servlet.ServletContext.getNamedDispatcher + .and(isPublic()), + RequestDispatcherTargetAdvice.class.getName()); + } + + public static class RequestDispatcherTargetAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void saveTarget( + @Advice.Argument(0) final String target, + @Advice.Return final RequestDispatcher dispatcher) { + InstrumentationContext.get(RequestDispatcher.class, String.class).put(dispatcher, target); + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterDecorator.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterDecorator.java new file mode 100644 index 0000000000..576fbe8e09 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterDecorator.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.servlet.filter; + +import datadog.trace.agent.decorator.BaseDecorator; + +public class FilterDecorator extends BaseDecorator { + public static final FilterDecorator DECORATE = new FilterDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet-filter"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-filter"; + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterInstrumentation.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterInstrumentation.java new file mode 100644 index 0000000000..6a5a98be45 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterInstrumentation.java @@ -0,0 +1,89 @@ +package datadog.trace.instrumentation.servlet.filter; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.servlet.filter.FilterDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.DDTags; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.servlet.Filter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class FilterInstrumentation extends Instrumenter.Default { + public FilterInstrumentation() { + super("servlet-filter"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", packageName + ".FilterDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.Filter"))); + } + + @Override + public Map, String> transformers() { + return singletonMap( + named("doFilter") + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + FilterAdvice.class.getName()); + } + + public static class FilterAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.This final Filter filter) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet.filter"); + DECORATE.afterStart(span); + + // Here we use "this" instead of "the method target" to distinguish abstract filter instances. + span.setTag(DDTags.RESOURCE_NAME, filter.getClass().getSimpleName() + ".doFilter"); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletDecorator.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletDecorator.java new file mode 100644 index 0000000000..f81a4a91d5 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletDecorator.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.servlet.http; + +import datadog.trace.agent.decorator.BaseDecorator; + +public class HttpServletDecorator extends BaseDecorator { + public static final HttpServletDecorator DECORATE = new HttpServletDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet-service"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-service"; + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java new file mode 100644 index 0000000000..dbcfcf488e --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java @@ -0,0 +1,97 @@ +package datadog.trace.instrumentation.servlet.http; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.servlet.http.HttpServletDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.DDTags; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.lang.reflect.Method; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class HttpServletInstrumentation extends Instrumenter.Default { + public HttpServletInstrumentation() { + super("servlet-service"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", packageName + ".HttpServletDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.http.HttpServlet"))); + } + + /** + * Here we are instrumenting the protected method for HttpServlet. This should ensure that this + * advice is always called after Servlet3Instrumentation which is instrumenting the public method. + */ + @Override + public Map, String> transformers() { + return singletonMap( + named("service") + .or(nameStartsWith("do")) // doGet, doPost, etc + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))) + .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse"))) + .and(isProtected().or(isPublic())), + HttpServletAdvice.class.getName()); + } + + public static class HttpServletAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.Origin final Method method) { + + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet." + method.getName()); + DECORATE.afterStart(span); + + // Here we use the Method instead of "this.class.name" to distinguish calls to "super". + span.setTag(DDTags.RESOURCE_NAME, DECORATE.spanNameForMethod(method)); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseDecorator.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseDecorator.java new file mode 100644 index 0000000000..be986c1ad3 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseDecorator.java @@ -0,0 +1,22 @@ +package datadog.trace.instrumentation.servlet.http; + +import datadog.trace.agent.decorator.BaseDecorator; + +public class HttpServletResponseDecorator extends BaseDecorator { + public static final HttpServletResponseDecorator DECORATE = new HttpServletResponseDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet", "servlet-response"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-response"; + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java new file mode 100644 index 0000000000..8f8c9717ec --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java @@ -0,0 +1,100 @@ +package datadog.trace.instrumentation.servlet.http; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.servlet.ServletRequestSetter.SETTER; +import static datadog.trace.instrumentation.servlet.http.HttpServletResponseDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class HttpServletResponseInstrumentation extends Instrumenter.Default { + public HttpServletResponseInstrumentation() { + super("servlet", "servlet-response"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.instrumentation.servlet.ServletRequestSetter", + "datadog.trace.agent.decorator.BaseDecorator", + packageName + ".HttpServletResponseDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()) + .and(safeHasSuperType(named("javax.servlet.http.HttpServletResponse"))); + } + + @Override + public Map, String> transformers() { + return singletonMap(named("sendError").or(named("sendRedirect")), SendAdvice.class.getName()); + } + + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + + public static class SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start( + @Advice.Origin("#m") final String method, @Advice.This final HttpServletResponse resp) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final HttpServletRequest req = + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class).get(resp); + if (req == null) { + // Missing the response->request linking... probably in a wrapped instance. + return null; + } + + final AgentSpan span = startSpan("servlet.response"); + DECORATE.afterStart(span); + + span.setTag(DDTags.RESOURCE_NAME, "HttpServletResponse." + method); + + // In case we lose context, inject trace into to the request. + propagate().inject(span, req, SETTER); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy b/dd-java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy new file mode 100644 index 0000000000..9a6b53ab2a --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy @@ -0,0 +1,106 @@ +import datadog.trace.agent.test.AgentTestRunner + +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class FilterTest extends AgentTestRunner { + static { + System.setProperty("dd.integration.servlet-filter.enabled", "true") + } + + def "test doFilter no-parent"() { + when: + filter.doFilter(null, null, null) + + then: + assertTraces(0) {} + + where: + filter = new TestFilter() + } + + def "test doFilter with parent"() { + when: + runUnderTrace("parent") { + filter.doFilter(null, null, null) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.filter" + resourceName "${filter.class.simpleName}.doFilter" + childOf span(0) + tags { + "component" "java-web-servlet-filter" + defaultTags() + } + } + } + } + + where: + filter << [new TestFilter(), new TestFilter() {}] + } + + def "test doFilter exception"() { + setup: + def ex = new Exception("some error") + def filter = new TestFilter() { + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { + throw ex + } + } + + when: + runUnderTrace("parent") { + filter.doFilter(null, null, null) + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, ex) + span(1) { + operationName "servlet.filter" + resourceName "${filter.class.simpleName}.doFilter" + childOf span(0) + errored true + tags { + "component" "java-web-servlet-filter" + defaultTags() + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestFilter implements Filter { + + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + } + + @Override + void destroy() { + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy new file mode 100644 index 0000000000..f82e5904ac --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy @@ -0,0 +1,282 @@ +import datadog.trace.agent.test.AgentTestRunner +import groovy.servlet.AbstractHttpServlet +import spock.lang.Subject + +import javax.servlet.ServletOutputStream +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.Cookie +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +import static java.util.Collections.emptyEnumeration + +class HttpServletResponseTest extends AgentTestRunner { + + @Subject + def response = new TestResponse() + def request = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + getHeaderNames() >> emptyEnumeration() + getAttributeNames() >> emptyEnumeration() + } + + def setup() { + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + assert response.__datadogContext$javax$servlet$http$HttpServletResponse != null + TEST_WRITER.clear() + } + + def "test send no-parent"() { + when: + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + + then: + assertTraces(0) {} + } + + def "test send with parent"() { + when: + runUnderTrace("parent") { + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + } + + then: + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.response" + resourceName "HttpServletResponse.sendRedirect" + childOf span(0) + tags { + "component" "java-web-servlet-response" + defaultTags() + } + } + span(2) { + operationName "servlet.response" + resourceName "HttpServletResponse.sendError" + childOf span(0) + tags { + "component" "java-web-servlet-response" + defaultTags() + } + } + span(3) { + operationName "servlet.response" + resourceName "HttpServletResponse.sendError" + childOf span(0) + tags { + "component" "java-web-servlet-response" + defaultTags() + } + } + } + } + } + + def "test send with exception"() { + setup: + def ex = new Exception("some error") + def response = new TestResponse() { + @Override + void sendRedirect(String s) { + throw ex + } + } + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + assert response.__datadogContext$javax$servlet$http$HttpServletResponse != null + TEST_WRITER.clear() + + when: + runUnderTrace("parent") { + response.sendRedirect("") + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, ex) + span(1) { + operationName "servlet.response" + resourceName "HttpServletResponse.sendRedirect" + childOf span(0) + errored true + tags { + "component" "java-web-servlet-response" + defaultTags() + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestResponse implements HttpServletResponse { + + @Override + void addCookie(Cookie cookie) { + + } + + @Override + boolean containsHeader(String s) { + return false + } + + @Override + String encodeURL(String s) { + return null + } + + @Override + String encodeRedirectURL(String s) { + return null + } + + @Override + String encodeUrl(String s) { + return null + } + + @Override + String encodeRedirectUrl(String s) { + return null + } + + @Override + void sendError(int i, String s) throws IOException { + + } + + @Override + void sendError(int i) throws IOException { + + } + + @Override + void sendRedirect(String s) throws IOException { + + } + + @Override + void setDateHeader(String s, long l) { + + } + + @Override + void addDateHeader(String s, long l) { + + } + + @Override + void setHeader(String s, String s1) { + + } + + @Override + void addHeader(String s, String s1) { + + } + + @Override + void setIntHeader(String s, int i) { + + } + + @Override + void addIntHeader(String s, int i) { + + } + + @Override + void setStatus(int i) { + + } + + @Override + void setStatus(int i, String s) { + + } + + @Override + String getCharacterEncoding() { + return null + } + + @Override + ServletOutputStream getOutputStream() throws IOException { + return null + } + + @Override + PrintWriter getWriter() throws IOException { + return null + } + + @Override + void setContentLength(int i) { + + } + + @Override + void setContentType(String s) { + + } + + @Override + void setBufferSize(int i) { + + } + + @Override + int getBufferSize() { + return 0 + } + + @Override + void flushBuffer() throws IOException { + + } + + @Override + void resetBuffer() { + + } + + @Override + boolean isCommitted() { + return false + } + + @Override + void reset() { + + } + + @Override + void setLocale(Locale locale) { + + } + + @Override + Locale getLocale() { + return null + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy new file mode 100644 index 0000000000..6df40c49a5 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy @@ -0,0 +1,126 @@ +import datadog.trace.agent.test.AgentTestRunner +import groovy.servlet.AbstractHttpServlet + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class HttpServletTest extends AgentTestRunner { + static { + System.setProperty("dd.integration.servlet-service.enabled", "true") + } + + def req = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + } + def resp = Mock(HttpServletResponse) + + def "test service no-parent"() { + when: + servlet.service(req, resp) + + then: + assertTraces(0) {} + + where: + servlet = new TestServlet() + } + + def "test service with parent"() { + when: + runUnderTrace("parent") { + servlet.service(req, resp) + } + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.service" + resourceName "HttpServlet.service" + childOf span(0) + tags { + "component" "java-web-servlet-service" + defaultTags() + } + } + span(2) { + operationName "servlet.doGet" + resourceName "${expectedResourceName}.doGet" + childOf span(1) + tags { + "component" "java-web-servlet-service" + defaultTags() + } + } + } + } + + where: + servlet << [new TestServlet(), new TestServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + } + }] + + expectedResourceName = servlet.class.anonymousClass ? servlet.class.name : servlet.class.simpleName + } + + def "test service exception"() { + setup: + def ex = new Exception("some error") + def servlet = new TestServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + throw ex + } + } + + when: + runUnderTrace("parent") { + servlet.service(req, resp) + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent", null, ex) + span(1) { + operationName "servlet.service" + resourceName "HttpServlet.service" + childOf span(0) + errored true + tags { + "component" "java-web-servlet-service" + defaultTags() + errorTags(ex.class, ex.message) + } + } + span(2) { + operationName "servlet.doGet" + resourceName "${servlet.class.name}.doGet" + childOf span(1) + errored true + tags { + "component" "java-web-servlet-service" + defaultTags() + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestServlet extends AbstractHttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + } + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy b/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy new file mode 100644 index 0000000000..f352051753 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy @@ -0,0 +1,95 @@ +import datadog.trace.agent.test.AgentTestRunner + +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class RequestDispatcherTest extends AgentTestRunner { + + def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse)) + + def "test dispatch no-parent"() { + when: + dispatcher.forward("") + dispatcher.include("") + + then: + assertTraces(0) {} + } + + def "test dispatcher #method with parent"() { + when: + runUnderTrace("parent") { + dispatcher."$method"(target) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.$operation" + resourceName target + childOf span(0) + tags { + "component" "java-web-servlet-dispatcher" + defaultTags() + } + } + } + } + + where: + operation | method + "forward" | "forward" + "forward" | "forwardNamed" + "include" | "include" + "include" | "includeNamed" + + target = "test-$method" + } + + def "test dispatcher #method exception"() { + setup: + def ex = new ServletException("some error") + def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse), ex) + + when: + runUnderTrace("parent") { + dispatcher."$method"(target) + } + + then: + def th = thrown(ServletException) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, ex) + span(1) { + operationName "servlet.$operation" + resourceName target + childOf span(0) + errored true + tags { + "component" "java-web-servlet-dispatcher" + defaultTags() + errorTags(ex.class, ex.message) + } + } + } + } + + where: + operation | method + "forward" | "forward" + "forward" | "forwardNamed" + "include" | "include" + "include" | "includeNamed" + + target = "test-$method" + } +} diff --git a/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java b/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java new file mode 100644 index 0000000000..2f08ad74ed --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java @@ -0,0 +1,181 @@ +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class RequestDispatcherUtils { + private final ServletRequest req; + private final ServletResponse resp; + private final ServletException toThrow; + + public RequestDispatcherUtils(final ServletRequest req, final ServletResponse resp) { + this.req = req; + this.resp = resp; + toThrow = null; + } + + public RequestDispatcherUtils( + final ServletRequest req, final ServletResponse resp, final ServletException toThrow) { + this.req = req; + this.resp = resp; + this.toThrow = toThrow; + } + + /* RequestDispatcher can't be visible to groovy otherwise things break, so everything is + * encapsulated in here where groovy doesn't need to access it. + */ + + void forward(final String target) throws ServletException, IOException { + new TestContext().getRequestDispatcher(target).forward(req, resp); + } + + void include(final String target) throws ServletException, IOException { + new TestContext().getRequestDispatcher(target).include(req, resp); + } + + void forwardNamed(final String target) throws ServletException, IOException { + new TestContext().getNamedDispatcher(target).forward(req, resp); + } + + void includeNamed(final String target) throws ServletException, IOException { + new TestContext().getNamedDispatcher(target).include(req, resp); + } + + class TestContext implements ServletContext { + @Override + public ServletContext getContext(final String s) { + return null; + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public String getMimeType(final String s) { + return null; + } + + @Override + public Set getResourcePaths(final String s) { + return null; + } + + @Override + public URL getResource(final String s) throws MalformedURLException { + return null; + } + + @Override + public InputStream getResourceAsStream(final String s) { + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(final String s) { + return new TestDispatcher(); + } + + @Override + public RequestDispatcher getNamedDispatcher(final String s) { + return new TestDispatcher(); + } + + @Override + public Servlet getServlet(final String s) throws ServletException { + return null; + } + + @Override + public Enumeration getServlets() { + return null; + } + + @Override + public Enumeration getServletNames() { + return null; + } + + @Override + public void log(final String s) {} + + @Override + public void log(final Exception e, final String s) {} + + @Override + public void log(final String s, final Throwable throwable) {} + + @Override + public String getRealPath(final String s) { + return null; + } + + @Override + public String getServerInfo() { + return null; + } + + @Override + public String getInitParameter(final String s) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + + @Override + public Object getAttribute(final String s) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public void setAttribute(final String s, final Object o) {} + + @Override + public void removeAttribute(final String s) {} + + @Override + public String getServletContextName() { + return null; + } + } + + class TestDispatcher implements RequestDispatcher { + @Override + public void forward(final ServletRequest servletRequest, final ServletResponse servletResponse) + throws ServletException, IOException { + if (toThrow != null) { + throw toThrow; + } + } + + @Override + public void include(final ServletRequest servletRequest, final ServletResponse servletResponse) + throws ServletException, IOException { + if (toThrow != null) { + throw toThrow; + } + } + } +}