Revert "Remove experimental jdbc and servlet integrations until further evaluation"

This reverts commit 2432a92230.
This commit is contained in:
Tyler Benson 2020-01-08 16:11:07 -08:00
parent 6b9c92eefa
commit 43fbf28035
19 changed files with 1550 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.sql.DataSource")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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();
}
}
}

View File

@ -5,8 +5,11 @@ import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.Config import datadog.trace.api.Config
import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDSpanTypes
import datadog.trace.instrumentation.api.Tags import datadog.trace.instrumentation.api.Tags
import javax.sql.DataSource
import org.apache.derby.jdbc.EmbeddedDataSource
import org.apache.derby.jdbc.EmbeddedDriver import org.apache.derby.jdbc.EmbeddedDriver
import org.h2.Driver import org.h2.Driver
import org.h2.jdbcx.JdbcDataSource
import org.hsqldb.jdbc.JDBCDriver import org.hsqldb.jdbc.JDBCDriver
import spock.lang.Shared import spock.lang.Shared
import spock.lang.Unroll 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 import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class JDBCInstrumentationTest extends AgentTestRunner { class JDBCInstrumentationTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.jdbc-beta.enabled", "true")
}
@Shared @Shared
def dbName = "jdbcUnitTest" 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" 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"() { def "test getClientInfo exception"() {
setup: setup:
Connection connection = new TestConnection(false) Connection connection = new TestConnection(false)

View File

@ -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" 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'
}

View File

@ -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<ServletRequest> {
public static final ServletRequestSetter SETTER = new ServletRequestSetter();
@Override
public void set(final ServletRequest carrier, final String key, final String value) {
carrier.setAttribute(key, value);
}
}

View File

@ -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";
}
}

View File

@ -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<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.RequestDispatcher")));
}
@Override
public Map<String, String> contextStore() {
return singletonMap("javax.servlet.RequestDispatcher", String.class.getName());
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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();
}
}
}

View File

@ -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<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.ServletContext")));
}
@Override
public Map<String, String> contextStore() {
return singletonMap("javax.servlet.RequestDispatcher", String.class.getName());
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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);
}
}
}

View File

@ -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";
}
}

View File

@ -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<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.Filter")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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();
}
}
}

View File

@ -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";
}
}

View File

@ -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<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, 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<String, String> 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();
}
}
}

View File

@ -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";
}
}

View File

@ -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<TypeDescription> typeMatcher() {
return not(isInterface())
.and(safeHasSuperType(named("javax.servlet.http.HttpServletResponse")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(named("sendError").or(named("sendRedirect")), SendAdvice.class.getName());
}
@Override
public Map<String, String> 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();
}
}
}

View File

@ -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() {
}
}
}

View File

@ -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
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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"
}
}

View File

@ -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;
}
}
}
}