Skip to content

Commit b40bcf9

Browse files
committed
Add automatic instrumentation for JDBC
This instrumentation creates spans for Statements and PreparedStatements. It also captures the corresponding SQL and additional connection info. ResultSet could be considered for future instrumentation to capture even more of the DB interaction time. This integration uses Bytebuddy instead of Byteman as the many methods to instrument would have been messy in Byteman.
1 parent 1826853 commit b40bcf9

File tree

11 files changed

+346
-31
lines changed

11 files changed

+346
-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/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.

dd-java-agent/src/main/java/com/datadoghq/agent/TracingAgent.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616
*/
1717
package com.datadoghq.agent;
1818

19+
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
20+
21+
import com.datadoghq.agent.instrumentation.Instrumenter;
1922
import java.lang.instrument.Instrumentation;
23+
import java.util.ServiceLoader;
2024
import lombok.extern.slf4j.Slf4j;
25+
import net.bytebuddy.agent.builder.AgentBuilder;
26+
import net.bytebuddy.description.type.TypeDescription;
27+
import net.bytebuddy.dynamic.DynamicType;
28+
import net.bytebuddy.utility.JavaModule;
2129

2230
/**
2331
* This class provides a wrapper around the ByteMan agent, to establish required system properties
@@ -27,12 +35,14 @@
2735
public class TracingAgent {
2836

2937
public static void premain(String agentArgs, final Instrumentation inst) throws Exception {
38+
addByteBuddy(inst);
3039
agentArgs = addManager(agentArgs);
3140
log.debug("Using premain for loading {}", TracingAgent.class.getSimpleName());
3241
org.jboss.byteman.agent.Main.premain(agentArgs, inst);
3342
}
3443

3544
public static void agentmain(String agentArgs, final Instrumentation inst) throws Exception {
45+
addByteBuddy(inst);
3646
agentArgs = addManager(agentArgs);
3747
log.debug("Using agentmain for loading {}", TracingAgent.class.getSimpleName());
3848
org.jboss.byteman.agent.Main.agentmain(agentArgs, inst);
@@ -48,4 +58,65 @@ protected static String addManager(String agentArgs) {
4858
log.debug("Agent args=: {}", agentArgs);
4959
return agentArgs;
5060
}
61+
62+
public static void addByteBuddy(final Instrumentation inst) {
63+
64+
AgentBuilder agentBuilder =
65+
new AgentBuilder.Default()
66+
.disableClassFormatChanges()
67+
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
68+
.with(new Listener())
69+
.ignore(nameStartsWith("com.datadoghq.agent.integration"));
70+
71+
for (final Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) {
72+
agentBuilder = instrumenter.instrument(agentBuilder);
73+
}
74+
75+
agentBuilder.installOn(inst);
76+
}
77+
78+
@Slf4j
79+
static class Listener implements AgentBuilder.Listener {
80+
81+
@Override
82+
public void onError(
83+
final String typeName,
84+
final ClassLoader classLoader,
85+
final JavaModule module,
86+
final boolean loaded,
87+
final Throwable throwable) {
88+
log.warn("Failed to handle " + typeName + " for transformation", throwable);
89+
}
90+
91+
@Override
92+
public void onTransformation(
93+
final TypeDescription typeDescription,
94+
final ClassLoader classLoader,
95+
final JavaModule module,
96+
final boolean loaded,
97+
final DynamicType dynamicType) {
98+
log.debug("Transformed {0}", typeDescription);
99+
}
100+
101+
@Override
102+
public void onIgnored(
103+
final TypeDescription typeDescription,
104+
final ClassLoader classLoader,
105+
final JavaModule module,
106+
final boolean loaded) {}
107+
108+
@Override
109+
public void onComplete(
110+
final String typeName,
111+
final ClassLoader classLoader,
112+
final JavaModule module,
113+
final boolean loaded) {}
114+
115+
@Override
116+
public void onDiscovery(
117+
final String typeName,
118+
final ClassLoader classLoader,
119+
final JavaModule module,
120+
final boolean loaded) {}
121+
}
51122
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.datadoghq.agent.instrumentation;
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder;
4+
5+
public interface Instrumenter {
6+
AgentBuilder instrument(AgentBuilder agentBuilder);
7+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.datadoghq.agent.instrumentation.jdbc;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
4+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
5+
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
6+
import static net.bytebuddy.matcher.ElementMatchers.named;
7+
import static net.bytebuddy.matcher.ElementMatchers.not;
8+
import static net.bytebuddy.matcher.ElementMatchers.returns;
9+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
10+
11+
import com.datadoghq.agent.instrumentation.Instrumenter;
12+
import com.google.auto.service.AutoService;
13+
import java.sql.Connection;
14+
import java.sql.PreparedStatement;
15+
import java.util.Map;
16+
import java.util.WeakHashMap;
17+
import net.bytebuddy.agent.builder.AgentBuilder;
18+
import net.bytebuddy.asm.Advice;
19+
20+
@AutoService(Instrumenter.class)
21+
public final class ConnectionInstrumentation implements Instrumenter {
22+
public static final Map<PreparedStatement, String> preparedStatements = new WeakHashMap<>();
23+
24+
@Override
25+
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
26+
return agentBuilder
27+
.type(not(isInterface()).and(hasSuperType(named(Connection.class.getName()))))
28+
.transform(
29+
new AgentBuilder.Transformer.ForAdvice()
30+
.advice(
31+
nameStartsWith("prepare")
32+
.and(takesArgument(0, String.class))
33+
.and(returns(PreparedStatement.class)),
34+
ConnectionAdvice.class.getName()));
35+
}
36+
37+
public static class ConnectionAdvice {
38+
@Advice.OnMethodExit
39+
public static void addDBInfo(
40+
@Advice.Argument(0) final String sql, @Advice.Return final PreparedStatement statement) {
41+
preparedStatements.put(statement, sql);
42+
}
43+
}
44+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.datadoghq.agent.instrumentation.jdbc;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
4+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
5+
import static net.bytebuddy.matcher.ElementMatchers.named;
6+
import static net.bytebuddy.matcher.ElementMatchers.not;
7+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
8+
9+
import com.datadoghq.agent.instrumentation.Instrumenter;
10+
import com.google.auto.service.AutoService;
11+
import java.sql.Connection;
12+
import java.sql.Driver;
13+
import java.util.Map;
14+
import java.util.Properties;
15+
import java.util.WeakHashMap;
16+
import lombok.Data;
17+
import net.bytebuddy.agent.builder.AgentBuilder;
18+
import net.bytebuddy.asm.Advice;
19+
20+
@AutoService(Instrumenter.class)
21+
public final class DriverInstrumentation implements Instrumenter {
22+
public static final Map<Connection, DBInfo> connectionInfo = new WeakHashMap<>();
23+
24+
@Override
25+
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
26+
return agentBuilder
27+
.type(not(isInterface()).and(hasSuperType(named(Driver.class.getName()))))
28+
.transform(
29+
new AgentBuilder.Transformer.ForAdvice()
30+
.advice(
31+
named("connect").and(takesArguments(String.class, Properties.class)),
32+
DriverAdvice.class.getName()));
33+
}
34+
35+
public static class DriverAdvice {
36+
@Advice.OnMethodExit
37+
public static void addDBInfo(
38+
@Advice.Argument(0) final String url,
39+
@Advice.Argument(1) final Properties info,
40+
@Advice.Return final Connection connection) {
41+
// Remove end of url to prevent passwords from leaking:
42+
final String sanitizedURL = url.replaceAll("[?;].*", "");
43+
final String type = url.split(":")[1];
44+
final String dbUser = info.getProperty("user");
45+
connectionInfo.put(connection, new DBInfo(sanitizedURL, type, dbUser));
46+
}
47+
}
48+
49+
@Data
50+
public static class DBInfo {
51+
private final String url;
52+
private final String type;
53+
private final String user;
54+
}
55+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.datadoghq.agent.instrumentation.jdbc;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
4+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
5+
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
6+
import static net.bytebuddy.matcher.ElementMatchers.named;
7+
import static net.bytebuddy.matcher.ElementMatchers.not;
8+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
9+
10+
import com.datadoghq.agent.instrumentation.Instrumenter;
11+
import com.google.auto.service.AutoService;
12+
import io.opentracing.ActiveSpan;
13+
import io.opentracing.NoopActiveSpanSource;
14+
import io.opentracing.tag.Tags;
15+
import io.opentracing.util.GlobalTracer;
16+
import java.sql.Connection;
17+
import java.sql.PreparedStatement;
18+
import java.util.Collections;
19+
import net.bytebuddy.agent.builder.AgentBuilder;
20+
import net.bytebuddy.asm.Advice;
21+
22+
@AutoService(Instrumenter.class)
23+
public final class PreparedStatementInstrumentation implements Instrumenter {
24+
25+
@Override
26+
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
27+
return agentBuilder
28+
.type(not(isInterface()).and(hasSuperType(named(PreparedStatement.class.getName()))))
29+
.transform(
30+
new AgentBuilder.Transformer.ForAdvice()
31+
.advice(
32+
nameStartsWith("execute").and(takesArguments(0)),
33+
PreparedStatementAdvice.class.getName()))
34+
.asDecorator();
35+
}
36+
37+
public static class PreparedStatementAdvice {
38+
39+
@Advice.OnMethodEnter
40+
public static ActiveSpan startSpan(@Advice.This final PreparedStatement statement) {
41+
// TODO: Should this happen always instead of just inside an existing tracer?
42+
if (GlobalTracer.get().activeSpan() == null) {
43+
return NoopActiveSpanSource.NoopActiveSpan.INSTANCE;
44+
}
45+
46+
final String sql = ConnectionInstrumentation.preparedStatements.get(statement);
47+
48+
final ActiveSpan span = GlobalTracer.get().buildSpan("sql.prepared_statement").startActive();
49+
Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT);
50+
Tags.COMPONENT.set(span, "java-jdbc");
51+
Tags.DB_STATEMENT.set(span, sql);
52+
span.setTag("span.origin.type", statement.getClass().getName());
53+
54+
try {
55+
final Connection connection = statement.getConnection();
56+
final DriverInstrumentation.DBInfo dbInfo =
57+
DriverInstrumentation.connectionInfo.get(connection);
58+
59+
span.setTag("db.jdbc.url", dbInfo.getUrl());
60+
span.setTag("db.schema", connection.getSchema());
61+
62+
Tags.DB_TYPE.set(span, dbInfo.getType());
63+
if (dbInfo.getUser() != null) {
64+
Tags.DB_USER.set(span, dbInfo.getUser());
65+
}
66+
} finally {
67+
return span;
68+
}
69+
}
70+
71+
@Advice.OnMethodExit(onThrowable = Throwable.class)
72+
public static void stopSpan(
73+
@Advice.Enter final ActiveSpan activeSpan,
74+
@Advice.Thrown(readOnly = false) final Throwable throwable) {
75+
if (throwable != null) {
76+
Tags.ERROR.set(activeSpan, true);
77+
activeSpan.log(Collections.singletonMap("error.object", throwable));
78+
}
79+
activeSpan.deactivate();
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)