Add span when calling DataSource.getConnection with a trace.
Disabled by default, and only creates a span if existing trace detected.
To enable:
* System Property: `-Ddd.integration.jdbc.enabled=true`
* Environment Variable: `DD_INTEGRATION_JDBC_ENABLED=true`
(Or more specifically with `jdbc-datasource`.)
ADD
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceDecorator.java
+package datadog.trace.instrumentation.jdbc;
+
+import datadog.trace.agent.decorator.BaseDecorator;
+
+public class DataSourceDecorator extends BaseDecorator {
+ public static final DataSourceDecorator DECORATE = new DataSourceDecorator();
+
+ @Override
+ protected String[] instrumentationNames() {
+ return new String[] {"jdbc", "jdbc-datasource"};
+ }
+
+ @Override
+ protected String component() {
+ return "java-jdbc-connection";
+ }
+
+ @Override
+ protected String spanType() {
+ return null;
+ }
+}

ADD
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DataSourceInstrumentation.java
+package datadog.trace.instrumentation.jdbc;
+
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.instrumentation.api.AgentTracer.activeSpan;
+import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
+import static datadog.trace.instrumentation.jdbc.DataSourceDecorator.DECORATE;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.api.DDTags;
+import datadog.trace.instrumentation.api.AgentScope;
+import datadog.trace.instrumentation.api.AgentSpan;
+import java.util.Map;
+import javax.sql.DataSource;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(Instrumenter.class)
+public final class DataSourceInstrumentation extends Instrumenter.Default {
+ public DataSourceInstrumentation() {
+ super("jdbc", "jdbc-datasource");
+ }
+
+ @Override
+ public boolean defaultEnabled() {
+ return false;
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ "datadog.trace.agent.decorator.BaseDecorator", packageName + ".DataSourceDecorator",
+ };
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return not(isInterface()).and(safeHasSuperType(named("javax.sql.DataSource")));
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(named("getConnection"), GetConnectionAdvice.class.getName());
+ }
+
+ public static class GetConnectionAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static AgentScope start(@Advice.This final DataSource ds) {
+ if (activeSpan() == null) {
+ // Don't want to generate a new top-level span
+ return null;
+ }
+
+ final AgentSpan span = startSpan("database.connection");
+ DECORATE.afterStart(span);
+
+ span.setTag(DDTags.RESOURCE_NAME, ds.getClass().getSimpleName() + ".getConnection");
+
+ return activateSpan(span, true).setAsyncPropagation(true);
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
+ if (scope == null) {
+ return;
+ }
+ DECORATE.onError(scope, throwable);
+ DECORATE.beforeFinish(scope);
+ scope.close();
+ }
+ }
+}

MODIFY
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DriverInstrumentation.java
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DriverInstrumentation.java
@@ -64,6 +64,10 @@ public final class DriverInstrumentation extends Instrumenter.Default {
@Advice.Argument(0) final String url,
@Advice.Argument(1) final Properties props,
@Advice.Return final Connection connection) {
+ if (connection == null) {
+ // Exception was probably thrown.
+ return;
+ }
final DBInfo dbInfo = JDBCConnectionUrlParser.parse(url, props);
JDBCMaps.connectionInfo.put(connection, dbInfo);
}

MODIFY
dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy
dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy
@@ -5,13 +5,15 @@ import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.Config
import datadog.trace.api.DDSpanTypes
import datadog.trace.instrumentation.api.Tags
+import javax.sql.DataSource
+import org.apache.derby.jdbc.EmbeddedDataSource
import org.apache.derby.jdbc.EmbeddedDriver
import org.h2.Driver
+import org.h2.jdbcx.JdbcDataSource
import org.hsqldb.jdbc.JDBCDriver
import spock.lang.Shared
import spock.lang.Unroll
-import javax.sql.DataSource
import java.sql.CallableStatement
import java.sql.Connection
import java.sql.PreparedStatement
@@ -19,9 +21,13 @@ import java.sql.ResultSet
import java.sql.Statement
import static datadog.trace.agent.test.utils.ConfigUtils.withConfigOverride
+import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class JDBCInstrumentationTest extends AgentTestRunner {
+ static {
+ System.setProperty("dd.integration.jdbc.enabled", "true")
+ }
@Shared
def dbName = "jdbcUnitTest"
@@ -166,10 +172,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
resultSet.getInt(1) == 3
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
serviceName renameService ? dbName.toLowerCase() : driver
operationName "${driver}.query"
@@ -229,10 +232,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
resultSet.getInt(1) == 3
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -284,10 +284,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
resultSet.getInt(1) == 3
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -339,10 +336,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
resultSet.getInt(1) == 3
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -394,10 +388,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
statement.updateCount == 0
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -452,10 +443,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
}
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -523,10 +511,7 @@ class JDBCInstrumentationTest extends AgentTestRunner {
rs.getInt(1) == 3
assertTraces(1) {
trace(0, 2) {
- span(0) {
- operationName "parent"
- parent()
- }
+ basicSpan(it, 0, "parent")
span(1) {
operationName "${driver}.query"
serviceName driver
@@ -569,6 +554,66 @@ class JDBCInstrumentationTest extends AgentTestRunner {
false | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
}
+ def "calling #datasource.class.simpleName getConnection generates a span when under existing trace"() {
+ setup:
+ assert datasource instanceof DataSource
+ init?.call(datasource)
+
+ when:
+ datasource.getConnection().close()
+
+ then:
+ !TEST_WRITER.any { it.any { it.operationName == "database.connection" } }
+ TEST_WRITER.clear()
+
+ when:
+ runUnderTrace("parent") {
+ datasource.getConnection().close()
+ }
+
+ then:
+ assertTraces(1) {
+ trace(0, recursive ? 3 : 2) {
+ basicSpan(it, 0, "parent")
+
+ span(1) {
+ operationName "database.connection"
+ resourceName "${datasource.class.simpleName}.getConnection"
+ childOf span(0)
+ tags {
+ "$Tags.COMPONENT" "java-jdbc-connection"
+ defaultTags()
+ }
+ }
+ if (recursive) {
+ span(2) {
+ operationName "database.connection"
+ resourceName "${datasource.class.simpleName}.getConnection"
+ childOf span(1)
+ tags {
+ "$Tags.COMPONENT" "java-jdbc-connection"
+ defaultTags()
+ }
+ }
+ }
+ }
+ }
+
+ where:
+ datasource | init
+ new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) }
+ new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") }
+ cpDatasources.get("hikari").get("h2") | null
+ cpDatasources.get("hikari").get("derby") | null
+ cpDatasources.get("c3p0").get("h2") | null
+ cpDatasources.get("c3p0").get("derby") | null
+
+ // Tomcat's pool doesn't work because the getConnection method is
+ // implemented in a parent class that doesn't implement DataSource
+
+ recursive = datasource instanceof EmbeddedDataSource
+ }
+
@Unroll
def "#connectionPoolName connections should be cached in case of wrapped connections"() {
setup:

Add span when calling DataSource.getConnection with a trace.

Disabled by default, and only creates a span if existing trace detected.

To enable:
* System Property: `-Ddd.integration.jdbc.enabled=true`
* Environment Variable: `DD_INTEGRATION_JDBC_ENABLED=true`
(Or more specifically with `jdbc-datasource`.)