JSP instrumentation
WIP: more unit tests to come
ADD
dd-java-agent/instrumentation/jsp/jsp.gradle
+apply plugin: 'version-scan'
+
+// TODO figure out the earliest version that can be supported, and the latest
+versionScan {
+ group = "org.apache.tomcat.embed"
+ module = 'tomcat-embed-jasper'
+ versions = "[8.0.41,)"
+ verifyPresent = [
+ "org.apache.jasper.servlet.JspServlet": null,
+ ]
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest {
+ dirName = 'test'
+ }
+}
+
+dependencies {
+ compileOnly group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '8.0.41'
+
+ compile project(':dd-trace-ot')
+ compile project(':dd-java-agent:agent-tooling')
+
+ compile deps.bytebuddy
+ compile deps.opentracing
+ annotationProcessor deps.autoservice
+ implementation deps.autoservice
+ testCompile project(':dd-java-agent:testing')
+
+ testCompile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.0.41'
+ testCompile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '8.0.41'
+
+ testCompile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.10.0'
+}
+
+configurations.latestDepTestCompile {
+ resolutionStrategy {
+ force group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '+'
+ force group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '+'
+ }
+}

ADD
dd-java-agent/instrumentation/jsp/src/main/java/datadog/trace/instrumentation/jsp/JasperJSPInstrumentation.java
+package datadog.trace.instrumentation.jsp;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+import static io.opentracing.log.Fields.ERROR_OBJECT;
+import static net.bytebuddy.matcher.ElementMatchers.*;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.DDAdvice;
+import datadog.trace.agent.tooling.DDTransformers;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.api.DDSpanTypes;
+import datadog.trace.api.DDTags;
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import java.util.Collections;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import org.apache.jasper.JspCompilationContext;
+import org.apache.jasper.servlet.JspServletWrapper;
+
+@AutoService(Instrumenter.class)
+public final class JasperJSPInstrumentation extends Instrumenter.Configurable {
+
+ public JasperJSPInstrumentation() {
+ super("jsp", "tomcat-jsp");
+ }
+
+ @Override
+ public AgentBuilder apply(final AgentBuilder agentBuilder) {
+ return agentBuilder
+ .type(
+ named("org.apache.jasper.servlet.JspServletWrapper"),
+ classLoaderHasClasses("org.apache.jasper.servlet.JspServlet"))
+ .transform(DDTransformers.defaultTransformers())
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("service")
+ .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest")))
+ .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse")))
+ .and(isPublic()),
+ JasperJSPAdvice.class.getName()))
+ .asDecorator();
+ }
+
+ public static class JasperJSPAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static Scope startSpan(
+ @Advice.Argument(0) final HttpServletRequest req,
+ @Advice.Argument(2) final boolean preCompile) {
+ final Scope scope =
+ GlobalTracer.get()
+ .buildSpan("jsp.service")
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
+ .withTag(DDTags.SPAN_TYPE, DDSpanTypes.WEB_SERVLET)
+ .withTag("servlet.context", req.getContextPath())
+ .startActive(true);
+
+ final Span span = scope.span();
+ span.setTag("jsp.precompile", preCompile);
+
+ Tags.COMPONENT.set(span, "tomcat-jsp-servlet");
+ Tags.HTTP_METHOD.set(span, req.getMethod());
+ Tags.HTTP_URL.set(span, req.getRequestURL().toString());
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Argument(1) final HttpServletResponse resp,
+ @Advice.This JspServletWrapper jspServletWrapper,
+ @Advice.Enter final Scope scope,
+ @Advice.Thrown final Throwable throwable) {
+
+ final Span span = scope.span();
+ JspCompilationContext jspCompilationContext = jspServletWrapper.getJspEngineContext();
+ if (jspCompilationContext != null) {
+ span.setTag("jsp.compiler", jspCompilationContext.getCompiler().getClass().getName());
+ span.setTag("jsp.outputDir", jspCompilationContext.getOutputDir());
+ span.setTag("jsp.classFQCN", jspCompilationContext.getFQCN());
+ if (throwable != null) {
+ span.setTag("jsp.classpath", jspCompilationContext.getClassPath());
+ }
+ }
+
+ if (throwable != null) {
+ if (resp.getStatus() == HttpServletResponse.SC_OK) {
+ // exception is thrown in filter chain, but status code is incorrect
+ Tags.HTTP_STATUS.set(span, 500);
+ }
+ Tags.ERROR.set(span, Boolean.TRUE);
+ span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
+ } else {
+ Tags.HTTP_STATUS.set(span, resp.getStatus());
+ }
+ scope.close();
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/jsp/src/test/groovy/JSPInstrumentationTest.groovy
+import com.google.common.io.Files
+import datadog.trace.agent.test.AgentTestRunner
+import datadog.trace.agent.test.TestUtils
+import datadog.trace.api.DDSpanTypes
+import io.netty.handler.codec.http.HttpResponseStatus
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.apache.catalina.Context
+import org.apache.catalina.startup.Tomcat
+import org.apache.tomcat.JarScanFilter
+import org.apache.tomcat.JarScanType
+import spock.lang.Shared
+
+import static datadog.trace.agent.test.ListWriterAssert.assertTraces
+
+class JSPInstrumentationTest extends AgentTestRunner {
+
+ static final int PORT = TestUtils.randomOpenPort()
+ OkHttpClient client = new OkHttpClient.Builder()
+ // Uncomment when debugging:
+// .connectTimeout(1, TimeUnit.HOURS)
+// .writeTimeout(1, TimeUnit.HOURS)
+// .readTimeout(1, TimeUnit.HOURS)
+ .build()
+
+ static Tomcat tomcatServer
+ static Context appContext
+ static final String JSP_WEBAPP_CONTEXT = "/jsptest-context"
+
+ @Shared
+ static File baseDir
+ static String expectedJspClassFilesDir = "/work/Tomcat/localhost$JSP_WEBAPP_CONTEXT/org/apache/jsp/"
+
+ def setupSpec() {
+ tomcatServer = new Tomcat()
+ tomcatServer.setPort(PORT)
+
+ baseDir = Files.createTempDir()
+ baseDir.deleteOnExit()
+ expectedJspClassFilesDir = baseDir.getCanonicalFile().getAbsolutePath() + expectedJspClassFilesDir
+ tomcatServer.setBaseDir(baseDir.getAbsolutePath())
+
+ appContext = tomcatServer.addWebapp(JSP_WEBAPP_CONTEXT,
+ JSPInstrumentationTest.getResource("/webapps/jsptest").getPath())
+ // Speed up startup by disabling jar scanning:
+ appContext.getJarScanner().setJarScanFilter(new JarScanFilter() {
+ @Override
+ boolean check(JarScanType jarScanType, String jarName) {
+ return false
+ }
+ })
+
+ tomcatServer.start()
+ System.out.println(
+ "Tomcat server: http://" + tomcatServer.getHost().getName() + ":" + PORT + "/")
+ }
+
+ def cleanupSpec() {
+ tomcatServer.stop()
+ tomcatServer.destroy()
+ }
+
+ def "basic looping jsp"() {
+ setup:
+ String url = "http://localhost:$PORT$JSP_WEBAPP_CONTEXT/loop.jsp"
+ def req = new Request.Builder().url(new URL(url)).get().build()
+
+ when:
+ Response res = client.newCall(req).execute()
+
+ then:
+ assertTraces(TEST_WRITER, 1) {
+ trace(0, 1) {
+ span(0) {
+ serviceName "jsptest-context"
+ operationName "jsp.service"
+ resourceName "GET /jsptest-context/loop.jsp"
+ spanType DDSpanTypes.WEB_SERVLET
+ errored false
+ tags {
+ "http.url" url
+ "http.method" "GET"
+ "span.kind" "server"
+ "component" "tomcat-jsp-servlet"
+ "span.type" DDSpanTypes.WEB_SERVLET
+ "servlet.context" JSP_WEBAPP_CONTEXT
+ "http.status_code" 200
+ "jsp.classFQCN" "org.apache.jsp.loop_jsp"
+ "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler"
+ "jsp.outputDir" expectedJspClassFilesDir
+ "jsp.precompile" false
+ defaultTags()
+ }
+ }
+ }
+ }
+ res.code() == HttpResponseStatus.OK.code()
+ }
+
+}

ADD
dd-java-agent/instrumentation/jsp/src/test/resources/webapps/jsptest/loop.jsp
+<html>
+<head><title>BASIC JSP</title></head>
+<body>
+ <%
+ for (int i = 0; i < 3; ++i) {
+ %>
+ <h2>number:<%= i %></h2><p></p>
+ <%
+ }
+ %>
+</body>
+</html>

MODIFY
settings.gradle
settings.gradle
@@ -32,6 +32,7 @@ include ':dd-java-agent:instrumentation:jedis-1.4'
include ':dd-java-agent:instrumentation:jetty-8'
include ':dd-java-agent:instrumentation:jms-1'
include ':dd-java-agent:instrumentation:jms-2'
+include ':dd-java-agent:instrumentation:jsp'
include ':dd-java-agent:instrumentation:kafka-clients-0.11'
include ':dd-java-agent:instrumentation:kafka-streams-0.11'
include ':dd-java-agent:instrumentation:lettuce-5'

JSP instrumentation

WIP: more unit tests to come