Skip to content

Commit 897b342

Browse files
authored
Merge pull request #135 from DataDog/tyler/servlet
Replace tomcat/jetty specific integ with generic servlet integ.
2 parents 20fb09c + 12323ce commit 897b342

File tree

23 files changed

+603
-278
lines changed

23 files changed

+603
-278
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,25 @@ Finally, add the following JVM argument when starting your application—in your
6464

6565
The Java Agent—once passed to your application—automatically traces requests to the frameworks, application servers, and databases shown below. It does this by using various libraries from [opentracing-contrib](https://github.com/opentracing-contrib). In most cases you don't need to install or configure anything; traces will automatically show up in your Datadog dashboards. The exception is [any database library that uses JDBC](#jdbc).
6666

67+
#### Application Servers
68+
69+
| Server | Versions | Comments |
70+
| ------------- |:-------------:| -----|
71+
| Java Servlet Compatible | 2.3+, 3.0+ | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers are linked |
72+
73+
*Note:* Many application servers are Servlet compatible such as Tomcat, Jetty, Websphere, Weblogic, etc.
74+
Also, frameworks like Spring Boot and Dropwizard inherently work because they use a Servlet compatible embedded application server.
75+
6776
#### Frameworks
6877

6978
| Framework | Versions | Comments |
7079
| ------------- |:-------------:| ----- |
71-
| [OkHTTP](https://github.com/opentracing-contrib/java-okhttp) | 3.x | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers |
72-
| [Apache HTTP Client](https://github.com/opentracing-contrib/java-apache-httpclient) | 4.3 + | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers|
80+
| [OkHTTP](https://github.com/opentracing-contrib/java-okhttp) | 3.x | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers are linked |
81+
| [Apache HTTP Client](https://github.com/opentracing-contrib/java-apache-httpclient) | 4.3 + | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers are linked|
7382
| [AWS SDK](https://github.com/opentracing-contrib/java-aws-sdk) | 1.11.0+ | Trace all client calls to any AWS service |
7483
| [Web Servlet Filters](https://github.com/opentracing-contrib/java-web-servlet-filter) | Depends on web server | See [Application Servers](#application-servers) |
7584
| [JMS 2](https://github.com/opentracing-contrib/java-jms) | 2.x | Trace calls to message brokers; distributed trace propagation not yet supported |
7685

77-
#### Application Servers
78-
79-
| Server | Versions | Comments |
80-
| ------------- |:-------------:| -----|
81-
| Jetty | 8.x, 9.x | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers |
82-
| Tomcat | 8.0.x, 8.5.x & 9.x | HTTP client calls with [cross-process](http://opentracing.io/documentation/pages/api/cross-process-tracing.html) headers |
83-
84-
Requests to any web frameworks that use these application servers—Dropwizard and Spring Boot, for example—are automatically traced as well.
85-
8686
#### Databases
8787

8888
| Database | Versions | Comments |
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.datadoghq.agent.integration.servlet
2+
3+
import com.datadoghq.trace.DDBaseSpan
4+
import com.datadoghq.trace.DDTags
5+
import com.datadoghq.trace.DDTracer
6+
import com.datadoghq.trace.writer.ListWriter
7+
import io.opentracing.tag.Tags
8+
import io.opentracing.util.GlobalTracer
9+
import okhttp3.Interceptor
10+
import okhttp3.OkHttpClient
11+
import okhttp3.Request
12+
import okhttp3.Response
13+
import org.eclipse.jetty.server.Server
14+
import org.eclipse.jetty.servlet.ServletContextHandler
15+
import spock.lang.Specification
16+
import spock.lang.Unroll
17+
18+
import java.lang.reflect.Field
19+
import java.util.concurrent.CountDownLatch
20+
21+
class JettyServletTest extends Specification {
22+
23+
static final int PORT = randomOpenPort()
24+
25+
// Jetty needs this to ensure consistent ordering for async.
26+
static CountDownLatch latch
27+
OkHttpClient client = new OkHttpClient.Builder()
28+
.addNetworkInterceptor(new Interceptor() {
29+
@Override
30+
Response intercept(Interceptor.Chain chain) throws IOException {
31+
def response = chain.proceed(chain.request())
32+
JettyServletTest.latch.await()
33+
return response
34+
}
35+
})
36+
// Uncomment when debugging:
37+
// .connectTimeout(1, TimeUnit.HOURS)
38+
// .writeTimeout(1, TimeUnit.HOURS)
39+
// .readTimeout(1, TimeUnit.HOURS)
40+
.build()
41+
42+
private Server jettyServer
43+
private ServletContextHandler servletContext
44+
45+
ListWriter writer = new ListWriter() {
46+
@Override
47+
void write(final List<DDBaseSpan<?>> trace) {
48+
add(trace)
49+
JettyServletTest.latch.countDown()
50+
}
51+
}
52+
DDTracer tracer = new DDTracer(writer)
53+
54+
def setup() {
55+
jettyServer = new Server(PORT)
56+
servletContext = new ServletContextHandler()
57+
58+
servletContext.addServlet(TestServlet.Sync, "/sync")
59+
servletContext.addServlet(TestServlet.Async, "/async")
60+
61+
jettyServer.setHandler(servletContext)
62+
jettyServer.start()
63+
64+
System.out.println(
65+
"Jetty server: http://localhost:" + PORT + "/")
66+
67+
try {
68+
GlobalTracer.register(tracer)
69+
} catch (final Exception e) {
70+
// Force it anyway using reflection
71+
final Field field = GlobalTracer.getDeclaredField("tracer")
72+
field.setAccessible(true)
73+
field.set(null, tracer)
74+
}
75+
writer.start()
76+
assert GlobalTracer.isRegistered()
77+
}
78+
79+
def cleanup() {
80+
jettyServer.stop()
81+
jettyServer.destroy()
82+
}
83+
84+
@Unroll
85+
def "test #path servlet call"() {
86+
setup:
87+
latch = new CountDownLatch(1)
88+
def request = new Request.Builder()
89+
.url("http://localhost:$PORT/$path")
90+
.get()
91+
.build()
92+
def response = client.newCall(request).execute()
93+
94+
expect:
95+
response.body().string().trim() == expectedResponse
96+
writer.size() == 2 // second (parent) trace is the okhttp call above...
97+
def trace = writer.firstTrace()
98+
trace.size() == 1
99+
def span = trace[0]
100+
101+
span.context().operationName == "servlet.request"
102+
!span.context().getErrorFlag()
103+
span.context().parentId != 0 // parent should be the okhttp call.
104+
span.context().tags[Tags.HTTP_URL.key] == "http://localhost:$PORT/$path"
105+
span.context().tags[Tags.HTTP_METHOD.key] == "GET"
106+
span.context().tags[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_SERVER
107+
span.context().tags[Tags.COMPONENT.key] == "java-web-servlet"
108+
span.context().tags[Tags.HTTP_STATUS.key] == 200
109+
span.context().tags[DDTags.THREAD_NAME] != null
110+
span.context().tags[DDTags.THREAD_ID] != null
111+
span.context().tags.size() == 7
112+
113+
where:
114+
path | expectedResponse
115+
"async" | "Hello Async"
116+
"sync" | "Hello Sync"
117+
}
118+
119+
private static int randomOpenPort() {
120+
new ServerSocket(0).withCloseable {
121+
it.setReuseAddress(true)
122+
return it.getLocalPort()
123+
}
124+
}
125+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.datadoghq.agent.integration.servlet
2+
3+
import groovy.servlet.AbstractHttpServlet
4+
5+
import javax.servlet.annotation.WebServlet
6+
import javax.servlet.http.HttpServletRequest
7+
import javax.servlet.http.HttpServletResponse
8+
9+
class TestServlet {
10+
11+
@WebServlet
12+
static class Sync extends AbstractHttpServlet {
13+
@Override
14+
void doGet(HttpServletRequest req, HttpServletResponse resp) {
15+
resp.writer.print("Hello Sync")
16+
}
17+
}
18+
19+
@WebServlet(asyncSupported = true)
20+
static class Async extends AbstractHttpServlet {
21+
@Override
22+
void doGet(HttpServletRequest req, HttpServletResponse resp) {
23+
Thread initialThread = Thread.currentThread()
24+
def context = req.startAsync()
25+
context.start {
26+
assert Thread.currentThread() != initialThread
27+
resp.writer.print("Hello Async")
28+
context.complete()
29+
}
30+
}
31+
}
32+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.datadoghq.agent.integration.servlet
2+
3+
import com.datadoghq.trace.DDTags
4+
import com.datadoghq.trace.DDTracer
5+
import com.datadoghq.trace.writer.ListWriter
6+
import com.google.common.io.Files
7+
import io.opentracing.tag.Tags
8+
import io.opentracing.util.GlobalTracer
9+
import okhttp3.OkHttpClient
10+
import okhttp3.Request
11+
import org.apache.catalina.Context
12+
import org.apache.catalina.startup.Tomcat
13+
import org.apache.tomcat.JarScanFilter
14+
import org.apache.tomcat.JarScanType
15+
import spock.lang.Specification
16+
import spock.lang.Unroll
17+
18+
import java.lang.reflect.Field
19+
20+
class TomcatServletTest extends Specification {
21+
22+
static final int PORT = randomOpenPort()
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+
Tomcat tomcatServer
31+
Context appContext
32+
33+
ListWriter writer = new ListWriter()
34+
DDTracer tracer = new DDTracer(writer)
35+
36+
def setup() {
37+
tomcatServer = new Tomcat()
38+
tomcatServer.setPort(PORT)
39+
40+
def baseDir = Files.createTempDir()
41+
baseDir.deleteOnExit()
42+
tomcatServer.setBaseDir(baseDir.getAbsolutePath())
43+
44+
final File applicationDir = new File(baseDir, "/webapps/ROOT")
45+
if (!applicationDir.exists()) {
46+
applicationDir.mkdirs()
47+
}
48+
appContext = tomcatServer.addWebapp("", applicationDir.getAbsolutePath())
49+
// Speed up startup by disabling jar scanning:
50+
appContext.getJarScanner().setJarScanFilter(new JarScanFilter(){
51+
@Override
52+
boolean check(JarScanType jarScanType, String jarName) {
53+
return false
54+
}
55+
})
56+
57+
Tomcat.addServlet(appContext, "syncServlet", new TestServlet.Sync())
58+
appContext.addServletMappingDecoded("/sync", "syncServlet")
59+
60+
Tomcat.addServlet(appContext, "asyncServlet", new TestServlet.Async())
61+
appContext.addServletMappingDecoded("/async", "asyncServlet")
62+
63+
tomcatServer.start()
64+
System.out.println(
65+
"Tomcat server: http://" + tomcatServer.getHost().getName() + ":" + PORT + "/")
66+
67+
68+
try {
69+
GlobalTracer.register(tracer)
70+
} catch (final Exception e) {
71+
// Force it anyway using reflection
72+
final Field field = GlobalTracer.getDeclaredField("tracer")
73+
field.setAccessible(true)
74+
field.set(null, tracer)
75+
}
76+
writer.start()
77+
assert GlobalTracer.isRegistered()
78+
}
79+
80+
def cleanup() {
81+
tomcatServer.stop()
82+
tomcatServer.destroy()
83+
}
84+
85+
@Unroll
86+
def "test #path servlet call"() {
87+
setup:
88+
def request = new Request.Builder()
89+
.url("http://localhost:$PORT/$path")
90+
.get()
91+
.build()
92+
def response = client.newCall(request).execute()
93+
94+
expect:
95+
response.body().string().trim() == expectedResponse
96+
writer.size() == 2 // second (parent) trace is the okhttp call above...
97+
def trace = writer.firstTrace()
98+
trace.size() == 1
99+
def span = trace[0]
100+
101+
span.context().operationName == "servlet.request"
102+
!span.context().getErrorFlag()
103+
span.context().parentId != 0 // parent should be the okhttp call.
104+
span.context().tags[Tags.HTTP_URL.key] == "http://localhost:$PORT/$path"
105+
span.context().tags[Tags.HTTP_METHOD.key] == "GET"
106+
span.context().tags[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_SERVER
107+
span.context().tags[Tags.COMPONENT.key] == "java-web-servlet"
108+
span.context().tags[Tags.HTTP_STATUS.key] == 200
109+
span.context().tags[DDTags.THREAD_NAME] != null
110+
span.context().tags[DDTags.THREAD_ID] != null
111+
span.context().tags.size() == 7
112+
113+
where:
114+
path | expectedResponse
115+
"async" | "Hello Async"
116+
"sync" | "Hello Sync"
117+
}
118+
119+
private static int randomOpenPort() {
120+
new ServerSocket(0).withCloseable {
121+
it.setReuseAddress(true)
122+
return it.getLocalPort()
123+
}
124+
}
125+
}

dd-java-agent-ittests/src/test/java/com/datadoghq/agent/TraceAnnotationsManagerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public void beforeTest() throws Exception {
2222
try {
2323
GlobalTracer.register(tracer);
2424
} catch (final Exception e) {
25-
// Force it anyway using reflexion
25+
// Force it anyway using reflection
2626
final Field field = GlobalTracer.class.getDeclaredField("tracer");
2727
field.setAccessible(true);
2828
field.set(null, tracer);

dd-java-agent-ittests/src/test/java/com/datadoghq/agent/integration/JettyServletInstrumentationTest.java

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)