Properly capture callable statement queries
CallableStatement is a sub type of PreparedStatement.  Previously we were only matching methods that returned exactly PreparedStatement.
MODIFY
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java
dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java
@@ -2,6 +2,7 @@ package datadog.trace.instrumentation.jdbc;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
@@ -36,7 +37,8 @@ public final class ConnectionInstrumentation extends Instrumenter.Default {
transformers.put(
nameStartsWith("prepare")
.and(takesArgument(0, String.class))
- .and(returns(PreparedStatement.class)),
+ // Also include CallableStatement, which is a sub type of PreparedStatement
+ .and(returns(isSubTypeOf(PreparedStatement.class))),
ConnectionPrepareAdvice.class.getName());
return transformers;
}

MODIFY
dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy
dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy
@@ -11,6 +11,7 @@ import spock.lang.Shared
import spock.lang.Unroll
import javax.sql.DataSource
+import java.sql.CallableStatement
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
@@ -311,6 +312,62 @@ class JDBCInstrumentationTest extends AgentTestRunner {
"derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
}
+ @Unroll
+ def "prepared call on #driver with #connection.getClass().getCanonicalName() generates a span"() {
+ setup:
+ CallableStatement statement = connection.prepareCall(query)
+ ResultSet resultSet = runUnderTrace("parent") {
+ return statement.executeQuery()
+ }
+
+ expect:
+ resultSet.next()
+ resultSet.getInt(1) == 3
+ assertTraces(1) {
+ trace(0, 2) {
+ span(0) {
+ operationName "parent"
+ parent()
+ }
+ span(1) {
+ operationName "${driver}.query"
+ serviceName driver
+ resourceName query
+ spanType DDSpanTypes.SQL
+ childOf span(0)
+ errored false
+ tags {
+ "db.type" driver
+ if (username != null) {
+ "db.user" username
+ }
+ "span.kind" Tags.SPAN_KIND_CLIENT
+ "span.type" DDSpanTypes.SQL
+ "component" "java-jdbc-prepared_statement"
+ "db.jdbc.url" jdbcUrls.get(driver)
+ "span.origin.type" String
+ defaultTags()
+ }
+ }
+ }
+ }
+
+ cleanup:
+ statement.close()
+ connection.close()
+
+ where:
+ driver | connection | username | query
+ "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "SELECT 3"
+ "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
+ "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3"
+ "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
+ "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3"
+ "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
+ "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3"
+ "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
+ }
+
@Unroll
def "statement update on #driver with #connection.getClass().getCanonicalName() generates a span"() {
setup:

Properly capture callable statement queries

CallableStatement is a sub type of PreparedStatement.  Previously we were only matching methods that returned exactly PreparedStatement.