Skip to content

Commit fc41529

Browse files
authored
Merge pull request #254 from JorgenG/feat/add-sparkjava-instrumentation
Spark Java Framework Instrumentation
2 parents f295b73 + 3db2d65 commit fc41529

13 files changed

Lines changed: 567 additions & 4 deletions

File tree

buildSrc/src/main/groovy/VersionScanPlugin.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ class VersionScanPlugin implements Plugin<Project> {
181181
}
182182
}
183183

184-
// println "Scanning ${includeVersionSet.size()} included and ${excludeVersionSet.size()} excluded versions. Included: ${includeVersionSet.collect { it.version }}}"
184+
// println "Scanning ${includeVersionSet.size()} included and ${excludeVersionSet.size()} excluded versions."
185+
// println "Included: ${includeVersionSet.collect { it.version }}}"
186+
// println "Excluded: ${excludeVersionSet.collect { it.version }}}"
185187

186188
includeVersionSet.each { version ->
187189
addScanTask("Include", new DefaultArtifact(version.groupId, version.artifactId, "jar", version.version), keyPresent, allInclude, project)

dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/classloading/ShadowPackageRenamingTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import spock.lang.Timeout
99

1010
import java.lang.reflect.Field
1111

12-
@Timeout(1)
12+
@Timeout(10)
1313
class ShadowPackageRenamingTest extends Specification {
1414
def "agent dependencies renamed"() {
1515
setup:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
apply plugin: 'version-scan'
2+
3+
versionScan {
4+
group = "org.eclipse.jetty"
5+
module = 'jetty-server'
6+
versions = "[8.0.0.v20110901,)"
7+
verifyMissing = [
8+
"org.eclipse.jetty.server.AsyncContext",
9+
]
10+
}
11+
12+
apply from: "${rootDir}/gradle/java.gradle"
13+
14+
dependencies {
15+
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1'
16+
compile('io.opentracing.contrib:opentracing-web-servlet-filter:0.1.0') {
17+
transitive = false
18+
}
19+
20+
compile project(':dd-trace-ot')
21+
compile project(':dd-java-agent:agent-tooling')
22+
23+
compile deps.bytebuddy
24+
compile deps.opentracing
25+
compile deps.autoservice
26+
27+
testCompile project(':dd-java-agent:testing')
28+
testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.0.0.v20110901'
29+
testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.0.0.v20110901'
30+
31+
testCompile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.6.0'
32+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package datadog.trace.instrumentation.jetty8;
2+
3+
import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
4+
import static io.opentracing.log.Fields.ERROR_OBJECT;
5+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
6+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
7+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.not;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
11+
12+
import com.google.auto.service.AutoService;
13+
import datadog.trace.agent.tooling.DDAdvice;
14+
import datadog.trace.agent.tooling.HelperInjector;
15+
import datadog.trace.agent.tooling.Instrumenter;
16+
import datadog.trace.api.DDSpanTypes;
17+
import datadog.trace.api.DDTags;
18+
import io.opentracing.Scope;
19+
import io.opentracing.Span;
20+
import io.opentracing.SpanContext;
21+
import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
22+
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
23+
import io.opentracing.propagation.Format;
24+
import io.opentracing.tag.Tags;
25+
import io.opentracing.util.GlobalTracer;
26+
import java.io.IOException;
27+
import java.util.Collections;
28+
import java.util.concurrent.atomic.AtomicBoolean;
29+
import javax.servlet.AsyncEvent;
30+
import javax.servlet.AsyncListener;
31+
import javax.servlet.http.HttpServletRequest;
32+
import javax.servlet.http.HttpServletResponse;
33+
import net.bytebuddy.agent.builder.AgentBuilder;
34+
import net.bytebuddy.asm.Advice;
35+
36+
@AutoService(Instrumenter.class)
37+
public final class HandlerInstrumentation extends Instrumenter.Configurable {
38+
public static final String SERVLET_OPERATION_NAME = "jetty.request";
39+
40+
public HandlerInstrumentation() {
41+
super("jetty", "jetty-8");
42+
}
43+
44+
@Override
45+
public boolean defaultEnabled() {
46+
return false;
47+
}
48+
49+
@Override
50+
public AgentBuilder apply(final AgentBuilder agentBuilder) {
51+
return agentBuilder
52+
.type(
53+
not(isInterface())
54+
.and(hasSuperType(named("org.eclipse.jetty.server.Handler")))
55+
.and(not(named("org.eclipse.jetty.server.handler.HandlerWrapper"))),
56+
not(classLoaderHasClasses("org.eclipse.jetty.server.AsyncContext")))
57+
.transform(
58+
new HelperInjector(
59+
"io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter",
60+
"io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter$MultivaluedMapFlatIterator",
61+
"io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator",
62+
"io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator$1",
63+
"io.opentracing.contrib.web.servlet.filter.TracingFilter",
64+
"io.opentracing.contrib.web.servlet.filter.TracingFilter$1",
65+
HandlerInstrumentationAdvice.class.getName() + "$TagSettingAsyncListener"))
66+
.transform(
67+
DDAdvice.create()
68+
.advice(
69+
named("handle")
70+
.and(takesArgument(0, named("java.lang.String")))
71+
.and(takesArgument(1, named("org.eclipse.jetty.server.Request")))
72+
.and(takesArgument(2, named("javax.servlet.http.HttpServletRequest")))
73+
.and(takesArgument(3, named("javax.servlet.http.HttpServletResponse")))
74+
.and(isPublic()),
75+
HandlerInstrumentationAdvice.class.getName()))
76+
.asDecorator();
77+
}
78+
79+
public static class HandlerInstrumentationAdvice {
80+
81+
@Advice.OnMethodEnter(suppress = Throwable.class)
82+
public static Scope startSpan(
83+
@Advice.This final Object source, @Advice.Argument(2) final HttpServletRequest req) {
84+
85+
if (GlobalTracer.get().activeSpan() != null) {
86+
// Tracing might already be applied. If so ignore this.
87+
return null;
88+
}
89+
90+
final SpanContext extractedContext =
91+
GlobalTracer.get()
92+
.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(req));
93+
final String resourceName = req.getMethod() + " " + source.getClass().getName();
94+
final Scope scope =
95+
GlobalTracer.get()
96+
.buildSpan(SERVLET_OPERATION_NAME)
97+
.asChildOf(extractedContext)
98+
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
99+
.withTag(DDTags.SPAN_TYPE, DDSpanTypes.WEB_SERVLET)
100+
.withTag("span.origin.type", source.getClass().getName())
101+
.startActive(false);
102+
103+
ServletFilterSpanDecorator.STANDARD_TAGS.onRequest(req, scope.span());
104+
Tags.COMPONENT.set(scope.span(), "jetty-handler");
105+
scope.span().setTag(DDTags.RESOURCE_NAME, resourceName);
106+
return scope;
107+
}
108+
109+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
110+
public static void stopSpan(
111+
@Advice.Argument(2) final HttpServletRequest req,
112+
@Advice.Argument(3) final HttpServletResponse resp,
113+
@Advice.Enter final Scope scope,
114+
@Advice.Thrown final Throwable throwable) {
115+
116+
if (scope != null) {
117+
final Span span = scope.span();
118+
if (throwable != null) {
119+
ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, throwable, span);
120+
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
121+
scope.close();
122+
scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
123+
} else if (req.isAsyncStarted()) {
124+
final AtomicBoolean activated = new AtomicBoolean(false);
125+
// what if async is already finished? This would not be called
126+
req.getAsyncContext().addListener(new TagSettingAsyncListener(activated, span));
127+
} else {
128+
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
129+
scope.close();
130+
scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
131+
}
132+
}
133+
}
134+
135+
public static class TagSettingAsyncListener implements AsyncListener {
136+
private final AtomicBoolean activated;
137+
private final Span span;
138+
139+
public TagSettingAsyncListener(final AtomicBoolean activated, final Span span) {
140+
this.activated = activated;
141+
this.span = span;
142+
}
143+
144+
@Override
145+
public void onComplete(final AsyncEvent event) throws IOException {
146+
if (activated.compareAndSet(false, true)) {
147+
try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
148+
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(
149+
(HttpServletRequest) event.getSuppliedRequest(),
150+
(HttpServletResponse) event.getSuppliedResponse(),
151+
span);
152+
}
153+
}
154+
}
155+
156+
@Override
157+
public void onTimeout(final AsyncEvent event) throws IOException {
158+
if (activated.compareAndSet(false, true)) {
159+
try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
160+
ServletFilterSpanDecorator.STANDARD_TAGS.onTimeout(
161+
(HttpServletRequest) event.getSuppliedRequest(),
162+
(HttpServletResponse) event.getSuppliedResponse(),
163+
event.getAsyncContext().getTimeout(),
164+
span);
165+
}
166+
}
167+
}
168+
169+
@Override
170+
public void onError(final AsyncEvent event) throws IOException {
171+
if (event.getThrowable() != null && activated.compareAndSet(false, true)) {
172+
try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
173+
ServletFilterSpanDecorator.STANDARD_TAGS.onError(
174+
(HttpServletRequest) event.getSuppliedRequest(),
175+
(HttpServletResponse) event.getSuppliedResponse(),
176+
event.getThrowable(),
177+
span);
178+
span.log(Collections.singletonMap(ERROR_OBJECT, event.getThrowable()));
179+
}
180+
}
181+
}
182+
183+
@Override
184+
public void onStartAsync(final AsyncEvent event) throws IOException {}
185+
}
186+
}
187+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import datadog.trace.agent.test.AgentTestRunner
2+
import datadog.trace.agent.test.TestUtils
3+
import datadog.trace.api.DDSpanTypes
4+
import okhttp3.OkHttpClient
5+
import org.eclipse.jetty.server.Handler
6+
import org.eclipse.jetty.server.Request
7+
import org.eclipse.jetty.server.Server
8+
import org.eclipse.jetty.server.handler.AbstractHandler
9+
10+
import javax.servlet.ServletException
11+
import javax.servlet.http.HttpServletRequest
12+
import javax.servlet.http.HttpServletResponse
13+
14+
class JettyHandlerTest extends AgentTestRunner {
15+
16+
static {
17+
System.setProperty("dd.integration.jetty.enabled", "true")
18+
}
19+
20+
int port = TestUtils.randomOpenPort()
21+
Server server = new Server(port)
22+
23+
OkHttpClient client = new OkHttpClient.Builder()
24+
// Uncomment when debugging:
25+
// .connectTimeout(1, TimeUnit.HOURS)
26+
// .writeTimeout(1, TimeUnit.HOURS)
27+
// .readTimeout(1, TimeUnit.HOURS)
28+
.build()
29+
30+
def cleanup() {
31+
server.stop()
32+
}
33+
34+
@Override
35+
void afterTest() {
36+
}
37+
38+
def "call to jetty creates a trace"() {
39+
setup:
40+
Handler handler = new AbstractHandler() {
41+
@Override
42+
void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
43+
response.setContentType("text/plain;charset=utf-8")
44+
response.setStatus(HttpServletResponse.SC_OK)
45+
baseRequest.setHandled(true)
46+
response.getWriter().println("Hello World")
47+
}
48+
}
49+
server.setHandler(handler)
50+
server.start()
51+
def request = new okhttp3.Request.Builder()
52+
.url("http://localhost:$port/")
53+
.get()
54+
.build()
55+
def response = client.newCall(request).execute()
56+
57+
expect:
58+
response.body().string().trim() == "Hello World"
59+
TEST_WRITER.waitForTraces(1)
60+
TEST_WRITER.size() == 1
61+
def trace = TEST_WRITER.firstTrace()
62+
trace.size() == 1
63+
def context = trace[0].context()
64+
context.serviceName == "unnamed-java-app"
65+
context.operationName == "jetty.request"
66+
context.resourceName == "GET ${handler.class.name}"
67+
context.spanType == DDSpanTypes.WEB_SERVLET
68+
!context.getErrorFlag()
69+
context.parentId == 0
70+
def tags = context.tags
71+
tags["http.url"] == "http://localhost:$port/"
72+
tags["http.method"] == "GET"
73+
tags["span.kind"] == "server"
74+
tags["span.type"] == "web"
75+
tags["component"] == "jetty-handler"
76+
tags["http.status_code"] == 200
77+
tags["thread.name"] != null
78+
tags["thread.id"] != null
79+
tags["span.origin.type"] == handler.class.name
80+
tags.size() == 9
81+
}
82+
83+
def "call to jetty with error creates a trace"() {
84+
setup:
85+
Handler handler = new AbstractHandler() {
86+
@Override
87+
void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
88+
throw new RuntimeException()
89+
}
90+
}
91+
server.setHandler(handler)
92+
server.start()
93+
def request = new okhttp3.Request.Builder()
94+
.url("http://localhost:$port/")
95+
.get()
96+
.build()
97+
def response = client.newCall(request).execute()
98+
99+
expect:
100+
response.body().string().trim() == ""
101+
TEST_WRITER.waitForTraces(1)
102+
TEST_WRITER.size() == 1
103+
def trace = TEST_WRITER.firstTrace()
104+
trace.size() == 1
105+
def context = trace[0].context()
106+
context.serviceName == "unnamed-java-app"
107+
context.operationName == "jetty.request"
108+
context.resourceName == "GET ${handler.class.name}"
109+
context.spanType == DDSpanTypes.WEB_SERVLET
110+
context.getErrorFlag()
111+
context.parentId == 0
112+
def tags = context.tags
113+
tags["http.url"] == "http://localhost:$port/"
114+
tags["http.method"] == "GET"
115+
tags["span.kind"] == "server"
116+
tags["span.type"] == "web"
117+
tags["component"] == "jetty-handler"
118+
tags["http.status_code"] == 500
119+
tags["thread.name"] != null
120+
tags["thread.id"] != null
121+
tags["span.origin.type"] == handler.class.name
122+
tags["error"] == true
123+
tags["error.type"] == RuntimeException.name
124+
tags["error.stack"] != null
125+
tags.size() == 12
126+
}
127+
}

dd-java-agent/instrumentation/servlet-3/servlet-3.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
compile deps.autoservice
2828

2929
testCompile project(':dd-java-agent:testing')
30+
testCompile project(':dd-java-agent:instrumentation:jetty-8') // See if there's any conflicts.
3031
testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.2.0.v20160908'
3132
testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.2.0.v20160908'
3233
testCompile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.0.41'

dd-java-agent/instrumentation/servlet-3/src/test/groovy/TomcatServletTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import spock.lang.Unroll
1616

1717
import java.lang.reflect.Field
1818

19-
@Timeout(5)
19+
@Timeout(15)
2020
class TomcatServletTest extends AgentTestRunner {
2121

2222
static final int PORT = TestUtils.randomOpenPort()

0 commit comments

Comments
 (0)