From 43fbf28035e062501ab2a88881eb2ad7d60158cc Mon Sep 17 00:00:00 2001 From: Tyler Benson Date: Wed, 8 Jan 2020 16:11:07 -0800 Subject: [PATCH] Revert "Remove experimental jdbc and servlet integrations until further evaluation" This reverts commit 2432a92230db759af80c8c8702485cf3df692766. --- .../jdbc/DataSourceDecorator.java | 22 ++ .../jdbc/DataSourceInstrumentation.java | 81 ++++++ .../groovy/JDBCInstrumentationTest.groovy | 66 +++++ .../instrumentation/servlet/servlet.gradle | 20 ++ .../servlet/ServletRequestSetter.java | 14 + .../RequestDispatcherDecorator.java | 22 ++ .../RequestDispatcherInstrumentation.java | 108 +++++++ .../ServletContextInstrumentation.java | 62 ++++ .../servlet/filter/FilterDecorator.java | 22 ++ .../servlet/filter/FilterInstrumentation.java | 89 ++++++ .../servlet/http/HttpServletDecorator.java | 22 ++ .../http/HttpServletInstrumentation.java | 112 +++++++ .../http/HttpServletResponseDecorator.java | 22 ++ .../HttpServletResponseInstrumentation.java | 105 +++++++ .../servlet/src/test/groovy/FilterTest.groovy | 105 +++++++ .../groovy/HttpServletResponseTest.groovy | 275 ++++++++++++++++++ .../src/test/groovy/HttpServletTest.groovy | 125 ++++++++ .../test/groovy/RequestDispatcherTest.groovy | 97 ++++++ .../test/groovy/RequestDispatcherUtils.java | 181 ++++++++++++ 19 files changed, 1550 insertions(+) create mode 100644 dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceDecorator.java create mode 100644 dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/ServletRequestSetter.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterDecorator.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/filter/FilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletDecorator.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseDecorator.java create mode 100644 dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java create mode 100644 dd-java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java 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..d4e75e1455 --- /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-beta", "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..43091ae449 --- /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-beta", "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..0560e5ec0c 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,11 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.instrumentation.api.Tags +import javax.sql.DataSource +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 +28,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-beta.enabled", "true") + } @Shared def dbName = "jdbcUnitTest" @@ -550,6 +556,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/servlet.gradle b/dd-java-agent/instrumentation/servlet/servlet.gradle index ff6901ae6f..1f4583dd52 100644 --- a/dd-java-agent/instrumentation/servlet/servlet.gradle +++ b/dd-java-agent/instrumentation/servlet/servlet.gradle @@ -1 +1,21 @@ +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' +} 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..1e0550cfbe --- /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-beta", "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..662ddc629b --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java @@ -0,0 +1,108 @@ +package datadog.trace.instrumentation.servlet.dispatcher; + +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-beta", "servlet-dispatcher"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @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); + + return activateSpan(span, true).setAsyncPropagation(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stop( + @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/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..3426135c6b --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java @@ -0,0 +1,62 @@ +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-beta", "servlet-dispatcher"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @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..c5dfc98f19 --- /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-beta", "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..f94814b908 --- /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-beta", "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..b4a8d93324 --- /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-beta", "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..faba631452 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java @@ -0,0 +1,112 @@ +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.bootstrap.InstrumentationContext; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.lang.reflect.Method; +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 HttpServletInstrumentation extends Instrumenter.Default { + public HttpServletInstrumentation() { + super("servlet-beta", "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()); + } + + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + + public static class HttpServletAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start( + @Advice.Origin final Method method, + @Advice.Argument(0) final HttpServletRequest request, + @Advice.Argument(1) final HttpServletResponse response) { + // For use by HttpServletResponseInstrumentation: + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class) + .put(response, request); + + 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..782aa99247 --- /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-beta", "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..68ae6400b0 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java @@ -0,0 +1,105 @@ +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-beta", "servlet-response"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @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..fc7ccb5aa5 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy @@ -0,0 +1,105 @@ +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-beta.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..9073a8044b --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy @@ -0,0 +1,275 @@ +import datadog.trace.agent.test.AgentTestRunner +import groovy.servlet.AbstractHttpServlet +import javax.servlet.ServletOutputStream +import javax.servlet.http.Cookie +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import spock.lang.Subject + +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class HttpServletResponseTest extends AgentTestRunner { + static { + System.setProperty("dd.integration.servlet-beta.enabled", "true") + } + + @Subject + def response = new TestResponse() + def request = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + } + + def setup() { + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service(request, response) + } + + 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(request, response) + + 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..1c5251509a --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy @@ -0,0 +1,125 @@ +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-beta.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..b5a5e0f3c6 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy @@ -0,0 +1,97 @@ +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 { + static { + System.setProperty("dd.integration.servlet-beta.enabled", "true") + } + + 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; + } + } + } +}