Skip to content

Commit 161d77b

Browse files
authored
Merge pull request #141 from DataDog/tyler/jdbc
Add automatic instrumentation for JDBC
2 parents 1826853 + 25029b4 commit 161d77b

File tree

14 files changed

+685
-31
lines changed

14 files changed

+685
-31
lines changed

README.md

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Finally, add the following JVM argument when starting your application—in your
6262
-javaagent:/path/to/the/dd-java-agent.jar
6363
```
6464

65-
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).
65+
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.
6666

6767
#### Application Servers
6868

@@ -87,8 +87,7 @@ Also, frameworks like Spring Boot and Dropwizard inherently work because they us
8787

8888
| Database | Versions | Comments |
8989
| ------------- |:-------------:| ----- |
90-
| Spring JDBC| 4.x | **NOT traced automatically**—see [JDBC instructions](#jdbc) |
91-
| Hibernate | 5.x | **NOT traced automatically**—see [JDBC instructions](#jdbc) |
90+
| JDBC | 4.x | Intercepts calls to JDBC compatible clients |
9291
| [MongoDB](https://github.com/opentracing-contrib/java-mongo-driver) | 3.x | Intercepts all the calls from the MongoDB client |
9392
| [Cassandra](https://github.com/opentracing-contrib/java-cassandra-driver) | 3.2.x | Intercepts all the calls from the Cassandra client |
9493

@@ -103,22 +102,6 @@ disabledInstrumentations: ["opentracing-apache-httpclient", "opentracing-mongo-d
103102
104103
See [this YAML file](dd-java-agent/src/main/resources/dd-trace-supported-framework.yaml) for the proper names of all supported libraries (i.e. the names as you must list them in `disabledInstrumentations`).
105104

106-
#### JDBC
107-
108-
The Java Agent doesn't automatically trace requests to databases whose drivers are JDBC-based. For such databases, you must:
109-
110-
1. Add the opentracing-jdbc dependency to your project, e.g. for Maven, add this to pom.xml:
111-
112-
```
113-
<dependency>
114-
<groupId>io.opentracing.contrib</groupId>
115-
<artifactId>opentracing-jdbc</artifactId>
116-
<version>0.0.3</version>
117-
</dependency>
118-
```
119-
120-
2. Modify your code's database connection strings, e.g. for a connection string `jdbc:h2:mem:test`, make it `jdbc:tracing:h2:mem:test`.
121-
122105
### The `@Trace` Annotation
123106

124107
The Java Agent lets you add a `@Trace` annotation to any method to measure its execution time. Setup the [Java Agent](#java-agent-setup) first if you haven't done so.

dd-java-agent-ittests/dd-java-agent-ittests.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ dependencies {
2828
testCompile group: 'javax.jms', name: 'javax.jms-api', version: '2.0.1'
2929
testCompile group: 'org.apache.activemq.tooling', name: 'activemq-junit', version: '5.14.5'
3030
testCompile group: 'org.apache.activemq', name: 'activemq-broker', version: '5.14.5'
31+
32+
// JDBC tests:
33+
testCompile group: 'com.h2database', name: 'h2', version: '1.4.196'
34+
testCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.4.0'
35+
testCompile group: 'org.apache.derby', name: 'derby', version: '10.14.1.0'
3136
}
3237

3338
configurations.all {
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package com.datadoghq.agent.integration.jdbc
2+
3+
import com.datadoghq.trace.DDTracer
4+
import com.datadoghq.trace.writer.ListWriter
5+
import io.opentracing.util.GlobalTracer
6+
import org.apache.derby.jdbc.EmbeddedDriver
7+
import org.h2.Driver
8+
import org.hsqldb.jdbc.JDBCDriver
9+
import spock.lang.Shared
10+
import spock.lang.Specification
11+
import spock.lang.Unroll
12+
13+
import java.lang.reflect.Field
14+
import java.sql.Connection
15+
import java.sql.PreparedStatement
16+
import java.sql.ResultSet
17+
import java.sql.Statement
18+
19+
class JDBCInstrumentationTest extends Specification {
20+
21+
ListWriter writer = new ListWriter()
22+
DDTracer tracer = new DDTracer(writer)
23+
24+
@Shared
25+
private Map<String, Connection> connections
26+
27+
def setupSpec() {
28+
Connection h2Connection = new Driver().connect("jdbc:h2:mem:integ-test", null)
29+
Connection hsqlConnection = new JDBCDriver().connect("jdbc:hsqldb:mem:integTest", null)
30+
Connection derbyConnection = new EmbeddedDriver().connect("jdbc:derby:memory:integTest;create=true", null)
31+
32+
connections = [
33+
h2 : h2Connection,
34+
derby : derbyConnection,
35+
hsqldb: hsqlConnection,
36+
]
37+
}
38+
39+
def cleanupSpec() {
40+
connections.values().each {
41+
it.close()
42+
}
43+
}
44+
45+
def setup() {
46+
try {
47+
GlobalTracer.register(tracer)
48+
} catch (final Exception e) {
49+
// Force it anyway using reflection
50+
final Field field = GlobalTracer.getDeclaredField("tracer")
51+
field.setAccessible(true)
52+
field.set(null, tracer)
53+
}
54+
writer.start()
55+
assert GlobalTracer.isRegistered()
56+
}
57+
58+
@Unroll
59+
def "basic statement on #driver generates spans"() {
60+
setup:
61+
Statement statement = connection.createStatement()
62+
ResultSet resultSet = statement.executeQuery(query)
63+
64+
expect:
65+
resultSet.next()
66+
resultSet.getInt(1) == 3
67+
writer.size() == 1
68+
69+
def trace = writer.firstTrace()
70+
trace.size() == 1
71+
def span = trace[0]
72+
73+
span.context().operationName == "${driver}.query"
74+
span.serviceName == driver
75+
span.resourceName == query
76+
span.type == "sql"
77+
!span.context().getErrorFlag()
78+
span.context().parentId == 0
79+
80+
81+
def tags = span.context().tags
82+
tags["db.type"] == driver
83+
tags["span.kind"] == "client"
84+
tags["component"] == "java-jdbc-statement"
85+
86+
tags["db.jdbc.url"].contains(driver)
87+
tags["span.origin.type"] != null
88+
89+
tags["thread.name"] != null
90+
tags["thread.id"] != null
91+
tags.size() == 7
92+
93+
cleanup:
94+
statement.close()
95+
96+
where:
97+
driver | connection | query
98+
"h2" | connections.get("h2") | "SELECT 3"
99+
"derby" | connections.get("derby") | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
100+
"hsqldb" | connections.get("hsqldb") | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS"
101+
}
102+
103+
@Unroll
104+
def "prepared statement execute on #driver generates a span"() {
105+
setup:
106+
PreparedStatement statement = connection.prepareStatement(query)
107+
assert statement.execute()
108+
ResultSet resultSet = statement.resultSet
109+
110+
expect:
111+
resultSet.next()
112+
resultSet.getInt(1) == 3
113+
writer.size() == 1
114+
115+
def trace = writer.firstTrace()
116+
trace.size() == 1
117+
def span = trace[0]
118+
119+
span.context().operationName == "${driver}.query"
120+
span.serviceName == driver
121+
span.resourceName == query
122+
span.type == "sql"
123+
!span.context().getErrorFlag()
124+
span.context().parentId == 0
125+
126+
127+
def tags = span.context().tags
128+
tags["db.type"] == driver
129+
tags["span.kind"] == "client"
130+
tags["component"] == "java-jdbc-prepared_statement"
131+
132+
tags["db.jdbc.url"].contains(driver)
133+
tags["span.origin.type"] != null
134+
135+
tags["thread.name"] != null
136+
tags["thread.id"] != null
137+
tags.size() == 7
138+
139+
cleanup:
140+
statement.close()
141+
142+
where:
143+
driver | connection | query
144+
"h2" | connections.get("h2") | "SELECT 3"
145+
"derby" | connections.get("derby") | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
146+
"hsqldb" | connections.get("hsqldb") | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS"
147+
}
148+
149+
@Unroll
150+
def "prepared statement query on #driver generates a span"() {
151+
setup:
152+
PreparedStatement statement = connection.prepareStatement(query)
153+
ResultSet resultSet = statement.executeQuery()
154+
155+
expect:
156+
resultSet.next()
157+
resultSet.getInt(1) == 3
158+
writer.size() == 1
159+
160+
def trace = writer.firstTrace()
161+
trace.size() == 1
162+
def span = trace[0]
163+
164+
span.context().operationName == "${driver}.query"
165+
span.serviceName == driver
166+
span.resourceName == query
167+
span.type == "sql"
168+
!span.context().getErrorFlag()
169+
span.context().parentId == 0
170+
171+
172+
def tags = span.context().tags
173+
tags["db.type"] == driver
174+
tags["span.kind"] == "client"
175+
tags["component"] == "java-jdbc-prepared_statement"
176+
177+
tags["db.jdbc.url"].contains(driver)
178+
tags["span.origin.type"] != null
179+
180+
tags["thread.name"] != null
181+
tags["thread.id"] != null
182+
tags.size() == 7
183+
184+
cleanup:
185+
statement.close()
186+
187+
where:
188+
driver | connection | query
189+
"h2" | connections.get("h2") | "SELECT 3"
190+
"derby" | connections.get("derby") | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
191+
"hsqldb" | connections.get("hsqldb") | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS"
192+
}
193+
194+
@Unroll
195+
def "statement update on #driver generates a span"() {
196+
setup:
197+
Statement statement = connection.createStatement()
198+
def sql = connection.nativeSQL(query)
199+
200+
expect:
201+
!statement.execute(sql)
202+
statement.updateCount == 0
203+
204+
writer.size() == 1
205+
206+
def trace = writer.firstTrace()
207+
trace.size() == 1
208+
def span = trace[0]
209+
210+
span.context().operationName == "${driver}.query"
211+
span.serviceName == driver
212+
span.resourceName == query
213+
span.type == "sql"
214+
!span.context().getErrorFlag()
215+
span.context().parentId == 0
216+
217+
218+
def tags = span.context().tags
219+
tags["db.type"] == driver
220+
tags["span.kind"] == "client"
221+
tags["component"] == "java-jdbc-statement"
222+
223+
tags["db.jdbc.url"].contains(driver)
224+
tags["span.origin.type"] != null
225+
226+
tags["thread.name"] != null
227+
tags["thread.id"] != null
228+
tags.size() == 7
229+
230+
cleanup:
231+
statement.close()
232+
233+
where:
234+
driver | connection | query
235+
"h2" | connections.get("h2") | "CREATE TABLE S_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))"
236+
"derby" | connections.get("derby") | "CREATE TABLE S_DERBY (id INTEGER not NULL, PRIMARY KEY ( id ))"
237+
"hsqldb" | connections.get("hsqldb") | "CREATE TABLE PUBLIC.S_HSQLDB (id INTEGER not NULL, PRIMARY KEY ( id ))"
238+
}
239+
240+
@Unroll
241+
def "prepared statement update on #driver generates a span"() {
242+
setup:
243+
def sql = connection.nativeSQL(query)
244+
PreparedStatement statement = connection.prepareStatement(sql)
245+
246+
expect:
247+
statement.executeUpdate() == 0
248+
writer.size() == 1
249+
250+
def trace = writer.firstTrace()
251+
trace.size() == 1
252+
def span = trace[0]
253+
254+
span.context().operationName == "${driver}.query"
255+
span.serviceName == driver
256+
span.resourceName == query
257+
span.type == "sql"
258+
!span.context().getErrorFlag()
259+
span.context().parentId == 0
260+
261+
262+
def tags = span.context().tags
263+
tags["db.type"] == driver
264+
tags["span.kind"] == "client"
265+
tags["component"] == "java-jdbc-prepared_statement"
266+
267+
tags["db.jdbc.url"].contains(driver)
268+
tags["span.origin.type"] != null
269+
270+
tags["thread.name"] != null
271+
tags["thread.id"] != null
272+
tags.size() == 7
273+
274+
cleanup:
275+
statement.close()
276+
277+
where:
278+
driver | connection | query
279+
"h2" | connections.get("h2") | "CREATE TABLE PS_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))"
280+
// Derby calls executeLargeUpdate from executeUpdate thus generating a nested span breaking this test.
281+
"hsqldb" | connections.get("hsqldb") | "CREATE TABLE PUBLIC.PS_HSQLDB (id INTEGER not NULL, PRIMARY KEY ( id ))"
282+
}
283+
}

dd-java-agent/dd-java-agent.gradle

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020
compile project(':dd-trace')
2121
compile project(':dd-trace-annotations')
2222

23+
compile group: 'net.bytebuddy', name: 'byte-buddy', version: '1.7.6'
2324
compile group: 'org.jboss.byteman', name: 'byteman', version: '3.0.10'
2425

2526
compile group: 'org.reflections', name: 'reflections', version: '0.9.11'
@@ -36,9 +37,6 @@ dependencies {
3637
testCompile(project(path: ':dd-java-agent:integrations:helpers')) {
3738
transitive = false
3839
}
39-
40-
// Not bundled in with the agent. Usage requires being on the app's classpath (eg. Spring Boot's executable jar)
41-
compileOnly group: 'io.opentracing.contrib', name: 'opentracing-jdbc', version: '0.0.3'
4240
}
4341

4442
project(':dd-java-agent:integrations:helpers').afterEvaluate { helperProject ->
@@ -90,6 +88,7 @@ shadowJar {
9088
relocate 'com.fasterxml', 'dd.deps.com.fasterxml'
9189

9290
relocate 'javassist', 'dd.deps.javassist'
91+
relocate 'net.bytebuddy', 'dd.deps.net.bytebuddy'
9392
relocate 'org.reflections', 'dd.deps.org.reflections'
9493
relocate('org.jboss.byteman', 'dd.deps.org.jboss.byteman') {
9594
// Renaming these causes a verify error in the tests.

0 commit comments

Comments
 (0)