diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/DBInfo.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/DBInfo.java index 086f5369f09..766ff355671 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/DBInfo.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/DBInfo.java @@ -87,7 +87,7 @@ public Builder type(String type) { this.type = type; // Those DBs use the full text of the query including the comments as a cache key, // so we disable full propagation support for them to avoid destroying the cache. - if (type.equals("oracle") || type.equals("sqlserver")) this.fullPropagationSupport = false; + if (type.equals("oracle")) this.fullPropagationSupport = false; return this; } diff --git a/dd-java-agent/instrumentation/jdbc/build.gradle b/dd-java-agent/instrumentation/jdbc/build.gradle index c0743218e02..5113b1e87bb 100644 --- a/dd-java-agent/instrumentation/jdbc/build.gradle +++ b/dd-java-agent/instrumentation/jdbc/build.gradle @@ -34,9 +34,11 @@ dependencies { testImplementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.23' testImplementation group: 'org.postgresql', name: 'postgresql', version: '[9.4,42.2.18]' + testImplementation group: 'com.microsoft.sqlserver', name: 'mssql-jdbc', version: '10.2.0.jre8' testImplementation group: 'org.testcontainers', name:'mysql', version: libs.versions.testcontainers.get() testImplementation group: 'org.testcontainers', name:'postgresql', version: libs.versions.testcontainers.get() + testImplementation group: 'org.testcontainers', name:'mssqlserver', version: '1.19.8' testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter') diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/AbstractPreparedStatementInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/AbstractPreparedStatementInstrumentation.java index e215cce37ea..ba0ecdece55 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/AbstractPreparedStatementInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/AbstractPreparedStatementInstrumentation.java @@ -5,6 +5,7 @@ import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; import static datadog.trace.instrumentation.jdbc.JDBCDecorator.DATABASE_QUERY; import static datadog.trace.instrumentation.jdbc.JDBCDecorator.DECORATE; +import static datadog.trace.instrumentation.jdbc.JDBCDecorator.INJECT_COMMENT; import static datadog.trace.instrumentation.jdbc.JDBCDecorator.logMissingQueryInfo; import static datadog.trace.instrumentation.jdbc.JDBCDecorator.logSQLException; import static net.bytebuddy.matcher.ElementMatchers.isPublic; @@ -16,6 +17,7 @@ import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.jdbc.DBInfo; import datadog.trace.bootstrap.instrumentation.jdbc.DBQueryInfo; import java.sql.Connection; @@ -64,21 +66,31 @@ public static AgentScope onEnter(@Advice.This final Statement statement) { return null; } try { - Connection connection = statement.getConnection(); - DBQueryInfo queryInfo = + final Connection connection = statement.getConnection(); + final DBQueryInfo queryInfo = InstrumentationContext.get(Statement.class, DBQueryInfo.class).get(statement); if (null == queryInfo) { logMissingQueryInfo(statement); return null; } - - final AgentSpan span = startSpan(DATABASE_QUERY); - DECORATE.afterStart(span); - DBInfo dbInfo = + final AgentSpan span; + final DBInfo dbInfo = JDBCDecorator.parseDBInfo( connection, InstrumentationContext.get(Connection.class, DBInfo.class)); + final boolean injectTraceContext = DECORATE.shouldInjectTraceContext(dbInfo); + + if (INJECT_COMMENT && injectTraceContext && DECORATE.isSqlServer(dbInfo)) { + // The span ID is pre-determined so that we can reference it when setting the context + final long spanID = DECORATE.setContextInfo(connection, dbInfo); + // we then force that pre-determined span ID for the span covering the actual query + span = AgentTracer.get().buildSpan(DATABASE_QUERY).withSpanId(spanID).start(); + } else { + span = startSpan(DATABASE_QUERY); + } + DECORATE.afterStart(span); DECORATE.onConnection(span, dbInfo); DECORATE.onPreparedStatement(span, queryInfo); + return activateSpan(span); } catch (SQLException e) { logSQLException(e); diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCDecorator.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCDecorator.java index cec448e20c9..62ff7ce3dac 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCDecorator.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCDecorator.java @@ -1,14 +1,18 @@ package datadog.trace.instrumentation.jdbc; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; import static datadog.trace.bootstrap.instrumentation.api.Tags.DB_OPERATION; import static datadog.trace.bootstrap.instrumentation.api.Tags.DB_SCHEMA; import static datadog.trace.bootstrap.instrumentation.api.Tags.DB_WAREHOUSE; import datadog.trace.api.Config; import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; import datadog.trace.api.naming.SpanNaming; import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -16,8 +20,11 @@ import datadog.trace.bootstrap.instrumentation.jdbc.DBInfo; import datadog.trace.bootstrap.instrumentation.jdbc.DBQueryInfo; import datadog.trace.bootstrap.instrumentation.jdbc.JDBCConnectionUrlParser; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.util.HashSet; @@ -235,6 +242,73 @@ public String traceParent(AgentSpan span, int samplingPriority) { return sb.toString(); } + public boolean isSqlServer(final DBInfo dbInfo) { + return "sqlserver".equals(dbInfo.getType()); + } + + /** + * Executes a `SET CONTEXT_INFO` statement on the DB with the active trace ID and the given span + * ID. This context will be "attached" to future queries on the same connection. See MSSQL + * doc. This is to be used where injecting trace and span in the comments with {@link + * SQLCommenter#inject} is not possible or convenient. + * + *

Upsides: still "visible" in sub-queries, does not bust caches based on full query text + * Downsides: takes time. + * + * @param connection The same connection as the one that will be used for the actual statement + * @param dbInfo dbInfo of the instrumented database + * @return spanID pre-created spanID + */ + public long setContextInfo(Connection connection, DBInfo dbInfo) { + final byte VERSION = 0; + final long spanID = Config.get().getIdGenerationStrategy().generateSpanId(); + AgentSpan instrumentationSpan = + AgentTracer.get().buildSpan("set context_info").withTag("dd.instrumentation", true).start(); + DECORATE.afterStart(instrumentationSpan); + DECORATE.onConnection(instrumentationSpan, dbInfo); + PreparedStatement instrumentationStatement = null; + try (AgentScope scope = activateSpan(instrumentationSpan)) { + final byte samplingDecision = + (byte) (instrumentationSpan.forceSamplingDecision() > 0 ? 1 : 0); + final byte versionAndSamplingDecision = + (byte) ((VERSION << 4) & 0b11110000 | samplingDecision & 0b00000001); + + ByteBuffer byteBuffer = ByteBuffer.allocate(1 + 3 * Long.BYTES); + byteBuffer.order(ByteOrder.BIG_ENDIAN); + + byteBuffer.put(versionAndSamplingDecision); + byteBuffer.putLong(spanID); + final DDTraceId traceId = instrumentationSpan.getTraceId(); + byteBuffer.putLong(traceId.toHighOrderLong()); + byteBuffer.putLong(traceId.toLong()); + final byte[] contextInfo = byteBuffer.array(); + + String instrumentationSql = "set context_info ?"; + instrumentationStatement = connection.prepareStatement(instrumentationSql); + instrumentationStatement.setBytes(1, contextInfo); + DECORATE.onStatement(instrumentationSpan, instrumentationSql); + instrumentationStatement.execute(); + } catch (Exception e) { + log.debug( + "Failed to set extra DBM data in context info for trace {}. " + + "To disable this behavior, set DBM_PROPAGATION_MODE to 'service' mode. " + + "See https://docs.datadoghq.com/database_monitoring/connect_dbm_and_apm/ for more info.{}", + instrumentationSpan.getTraceId().toHexString(), + e); + DECORATE.onError(instrumentationSpan, e); + } finally { + if (instrumentationStatement != null) { + try { + instrumentationStatement.close(); + } catch (Exception e) { + } + } + instrumentationSpan.finish(); + } + return spanID; + } + @Override protected void postProcessServiceAndOperationName( AgentSpan span, DatabaseClientDecorator.NamingEntry namingEntry) { diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java index aee513deb7f..b684aaae783 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java @@ -21,6 +21,7 @@ import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.jdbc.DBInfo; import java.sql.Connection; import java.sql.SQLException; @@ -84,18 +85,29 @@ public static AgentScope onEnter( } try { final Connection connection = statement.getConnection(); - final AgentSpan span = startSpan(DATABASE_QUERY); - DECORATE.afterStart(span); final DBInfo dbInfo = JDBCDecorator.parseDBInfo( connection, InstrumentationContext.get(Connection.class, DBInfo.class)); + boolean injectTraceContext = DECORATE.shouldInjectTraceContext(dbInfo); + final AgentSpan span; + final boolean isSqlServer = DECORATE.isSqlServer(dbInfo); + + if (isSqlServer && INJECT_COMMENT && injectTraceContext) { + // The span ID is pre-determined so that we can reference it when setting the context + final long spanID = DECORATE.setContextInfo(connection, dbInfo); + // we then force that pre-determined span ID for the span covering the actual query + span = AgentTracer.get().buildSpan(DATABASE_QUERY).withSpanId(spanID).start(); + } else { + span = startSpan(DATABASE_QUERY); + } + + DECORATE.afterStart(span); DECORATE.onConnection(span, dbInfo); final String copy = sql; if (span != null && INJECT_COMMENT) { String traceParent = null; - boolean injectTraceContext = DECORATE.shouldInjectTraceContext(dbInfo); - if (injectTraceContext) { + if (injectTraceContext && !isSqlServer) { Integer priority = span.forceSamplingDecision(); if (priority != null) { traceParent = DECORATE.traceParent(span, priority); diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCDecoratorTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCDecoratorTest.groovy index 6128e0bd5ed..69de0441f8a 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCDecoratorTest.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCDecoratorTest.groovy @@ -21,7 +21,7 @@ abstract class JDBCDecoratorTest extends AgentTestRunner { where: dbType | expectedByType "oracle" | false - "sqlserver" | false + "sqlserver" | true "mysql" | true "postgresql" | true } diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/RemoteJDBCInstrumentationTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/RemoteJDBCInstrumentationTest.groovy index b5a798b5536..e69dc8f9121 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/RemoteJDBCInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/RemoteJDBCInstrumentationTest.groovy @@ -1,12 +1,15 @@ import com.mchange.v2.c3p0.ComboPooledDataSource +import com.microsoft.sqlserver.jdbc.SQLServerException import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import datadog.trace.agent.test.naming.VersionedNamingTestBase import datadog.trace.agent.test.utils.PortUtils import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes +import datadog.trace.api.naming.v1.DatabaseNamingV1 import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags import datadog.trace.bootstrap.instrumentation.api.Tags +import org.testcontainers.containers.MSSQLServerContainer import org.testcontainers.containers.MySQLContainer import org.testcontainers.containers.PostgreSQLContainer import spock.lang.Requires @@ -29,50 +32,67 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST // workaround for SSLHandShakeException on J9 only with Hikari/MySQL @Requires({ !System.getProperty("java.vendor").contains("IBM") }) abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { + static final String POSTGRESQL = "postgresql" + static final String MYSQL = "mysql" + static final String SQLSERVER = "sqlserver" + @Shared - def dbName = "jdbcUnitTest" + private Map dbName = [ + (POSTGRESQL): "jdbcUnitTest", + (MYSQL) : "jdbcUnitTest", + (SQLSERVER) : "master" + ] @Shared private Map jdbcUrls = [ - "postgresql": "jdbc:postgresql://localhost:5432/$dbName", - "mysql" : "jdbc:mysql://localhost:3306/$dbName" + "postgresql" : "jdbc:postgresql://localhost:5432/" + dbName.get("postgresql"), + "mysql" : "jdbc:mysql://localhost:3306/" + dbName.get("mysql"), + "sqlserver" : "jdbc:sqlserver://localhost:1433/" + dbName.get("sqlserver"), ] @Shared private Map jdbcDriverClassNames = [ "postgresql": "org.postgresql.Driver", - "mysql" : "com.mysql.jdbc.Driver" + "mysql" : "com.mysql.jdbc.Driver", + "sqlserver" : "com.microsoft.sqlserver.jdbc.SQLServerDriver", ] @Shared private Map jdbcUserNames = [ "postgresql": "sa", - "mysql" : "sa" + "mysql" : "sa", + "sqlserver" : "sa", ] @Shared private Map jdbcPasswords = [ "mysql" : "sa", - "postgresql": "sa" + "postgresql": "sa", + "sqlserver" : "Datad0g_", ] @Shared def postgres @Shared def mysql - @Shared - private Properties peerConnectionProps = { - def props = new Properties() - props.setProperty("user", "sa") - props.setProperty("password", "sa") - return props - }() + def sqlserver // JDBC Connection pool name (i.e. HikariCP) -> Map @Shared private Map> cpDatasources = new HashMap<>() + def peerConnectionProps(String db){ + def props = new Properties() + props.setProperty("user", jdbcUserNames.get(db)) + props.setProperty("password", jdbcPasswords.get(db)) + return props + } + + protected getDbType(String dbType){ + return dbType + } + def prepareConnectionPoolDatasources() { String[] connectionPoolNames = ["tomcat", "hikari", "c3p0",] connectionPoolNames.each { cpName -> @@ -152,17 +172,21 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { def setupSpec() { postgres = new PostgreSQLContainer("postgres:11.1") - .withDatabaseName(dbName).withUsername("sa").withPassword("sa") + .withDatabaseName(dbName.get(POSTGRESQL)).withUsername(jdbcUserNames.get(POSTGRESQL)).withPassword(jdbcPasswords.get(POSTGRESQL)) postgres.start() PortUtils.waitForPortToOpen(postgres.getHost(), postgres.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), 5, TimeUnit.SECONDS) - jdbcUrls.put("postgresql", "${postgres.getJdbcUrl()}") + jdbcUrls.put(POSTGRESQL, "${postgres.getJdbcUrl()}") mysql = new MySQLContainer("mysql:8.0") - .withDatabaseName(dbName).withUsername("sa").withPassword("sa") + .withDatabaseName(dbName.get(MYSQL)).withUsername(jdbcUserNames.get(MYSQL)).withPassword(jdbcPasswords.get(MYSQL)) // https://github.com/testcontainers/testcontainers-java/issues/914 mysql.addParameter("TC_MY_CNF", null) mysql.start() PortUtils.waitForPortToOpen(mysql.getHost(), mysql.getMappedPort(MySQLContainer.MYSQL_PORT), 5, TimeUnit.SECONDS) - jdbcUrls.put("mysql", "${mysql.getJdbcUrl()}") + jdbcUrls.put(MYSQL, "${mysql.getJdbcUrl()}") + sqlserver = new MSSQLServerContainer().acceptLicense().withPassword(jdbcPasswords.get(SQLSERVER)) + sqlserver.start() + PortUtils.waitForPortToOpen(sqlserver.getHost(), sqlserver.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT), 5, TimeUnit.SECONDS) + jdbcUrls.put(SQLSERVER, "${sqlserver.getJdbcUrl()};DatabaseName=${dbName.get(SQLSERVER)}") prepareConnectionPoolDatasources() } @@ -177,6 +201,7 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { } postgres?.close() mysql?.close() + sqlserver?.close() } def "basic statement with #connection.getClass().getCanonicalName() on #driver generates spans"() { @@ -194,33 +219,82 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { def addDbmTag = dbmTraceInjected() resultSet.next() resultSet.getInt(1) == 3 - assertTraces(1) { - trace(2) { - basicSpan(it, "parent") - span { - serviceName renameService ? dbName.toLowerCase() : service(driver) - operationName this.operation(driver) - resourceName obfuscatedQuery - spanType DDSpanTypes.SQL - childOf span(0) - errored false - measured true - tags { - "$Tags.COMPONENT" "java-jdbc-statement" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.DB_TYPE" driver - "$Tags.DB_INSTANCE" dbName.toLowerCase() - "$Tags.PEER_HOSTNAME" String - // currently there is a bug in the instrumentation with - // postgresql and mysql if the connection event is missed - // since Connection.getClientInfo will not provide the username - "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } - "$Tags.DB_OPERATION" operation - if (addDbmTag) { - "$InstrumentationTags.DBM_TRACE_INJECTED" true + if (driver == POSTGRESQL || driver == MYSQL || !addDbmTag) { + assertTraces(1) { + trace(2) { + basicSpan(it, "parent") + span { + serviceName renameService ? dbName.get(driver).toLowerCase() : service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + if (addDbmTag) { + "$InstrumentationTags.DBM_TRACE_INJECTED" true + } + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + } + } + } else { + assertTraces(1) { + trace(3) { + basicSpan(it, "parent") + span { + serviceName renameService ? dbName.get(driver).toLowerCase() : service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + span { + serviceName renameService ? dbName.get(driver).toLowerCase() : service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName "set context_info ?" + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" "set" + "dd.instrumentation" true + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() } - peerServiceFrom(Tags.DB_INSTANCE) - defaultTags() } } } @@ -231,15 +305,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | renameService | query | operation | obfuscatedQuery - "mysql" | connectTo(driver, peerConnectionProps) | false | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | connectTo(driver, peerConnectionProps) | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" + driver | connection | renameService | query | operation | obfuscatedQuery + MYSQL | connectTo(driver, peerConnectionProps(driver)) | false | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | false | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | false | "SELECT 3 FROM pg_user" | "SELECT" | "SELECT ? FROM pg_user" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | false | "SELECT 3" | "SELECT" | "SELECT ?" } def "prepared statement execute on #driver with #connection.getClass().getCanonicalName() generates a span"() { @@ -256,31 +334,80 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { then: resultSet.next() resultSet.getInt(1) == 3 - assertTraces(1) { - trace(2) { - basicSpan(it, "parent") - span { - operationName this.operation(driver) - serviceName service(driver) - resourceName obfuscatedQuery - spanType DDSpanTypes.SQL - childOf span(0) - errored false - measured true - tags { - "$Tags.COMPONENT" "java-jdbc-prepared_statement" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.DB_TYPE" driver - "$Tags.DB_INSTANCE" dbName.toLowerCase() - // only set when there is an out of proc instance (postgresql, mysql) - "$Tags.PEER_HOSTNAME" String - // currently there is a bug in the instrumentation with - // postgresql and mysql if the connection event is missed - // since Connection.getClientInfo will not provide the username - "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } - "$Tags.DB_OPERATION" operation - peerServiceFrom(Tags.DB_INSTANCE) - defaultTags() + if (driver == POSTGRESQL || driver == MYSQL || !dbmTraceInjected()) { + assertTraces(1) { + trace(2) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + } + } + } else { + assertTraces(1) { + trace(3) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + span { + serviceName service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName "set context_info ?" + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" "set" + "dd.instrumentation" true + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } } } } @@ -291,15 +418,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query | operation | obfuscatedQuery - "mysql" | connectTo(driver, peerConnectionProps) | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | connectTo(driver, peerConnectionProps) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + driver | connection | query | operation | obfuscatedQuery + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" } def "prepared statement query on #driver with #connection.getClass().getCanonicalName() generates a span"() { @@ -314,31 +445,84 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { then: resultSet.next() resultSet.getInt(1) == 3 - assertTraces(1) { - trace(2) { - basicSpan(it, "parent") - span { - operationName this.operation(driver) - serviceName service(driver) - resourceName obfuscatedQuery - spanType DDSpanTypes.SQL - childOf span(0) - errored false - measured true - tags { - "$Tags.COMPONENT" "java-jdbc-prepared_statement" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.DB_TYPE" driver - "$Tags.DB_INSTANCE" dbName.toLowerCase() - // only set when there is an out of proc instance (postgresql, mysql) - "$Tags.PEER_HOSTNAME" String - // currently there is a bug in the instrumentation with - // postgresql and mysql if the connection event is missed - // since Connection.getClientInfo will not provide the username - "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } - "$Tags.DB_OPERATION" operation - peerServiceFrom(Tags.DB_INSTANCE) - defaultTags() + if (driver == POSTGRESQL || driver == MYSQL || !dbmTraceInjected()) { + assertTraces(1) { + trace(2) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + } + } + } else { + assertTraces(1) { + trace(3) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + span { + serviceName service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName "set context_info ?" + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" "set" + "dd.instrumentation" true + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } } } } @@ -349,15 +533,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query | operation | obfuscatedQuery - "mysql" | connectTo(driver, peerConnectionProps) | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | connectTo(driver, peerConnectionProps) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + driver | connection | query | operation | obfuscatedQuery + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" } def "prepared call on #driver with #connection.getClass().getCanonicalName() generates a span"() { @@ -372,30 +560,78 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { then: resultSet.next() resultSet.getInt(1) == 3 - assertTraces(1) { - trace(2) { - basicSpan(it, "parent") - span { - operationName this.operation(driver) - serviceName service(driver) - resourceName obfuscatedQuery - spanType DDSpanTypes.SQL - childOf span(0) - errored false - measured true - tags { - "$Tags.COMPONENT" "java-jdbc-prepared_statement" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.DB_TYPE" driver - "$Tags.DB_INSTANCE" dbName.toLowerCase() - // only set when there is an out of proc instance (postgresql, mysql) - "$Tags.PEER_HOSTNAME" String - // currently there is a bug in the instrumentation with - // postgresql and mysql if the connection event is missed - // since Connection.getClientInfo will not provide the username - "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } - "${Tags.DB_OPERATION}" operation - defaultTags() + if (driver == POSTGRESQL || driver == MYSQL || !dbmTraceInjected()) { + assertTraces(1) { + trace(2) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "${Tags.DB_OPERATION}" operation + defaultTags() + } + } + } + } + } else { + assertTraces(1) { + trace(3) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName obfuscatedQuery + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-prepared_statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "${Tags.DB_OPERATION}" operation + defaultTags() + } + } + span { + serviceName service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName "set context_info ?" + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" "set" + "dd.instrumentation" true + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } } } } @@ -406,15 +642,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query | operation | obfuscatedQuery - "mysql" | connectTo(driver, peerConnectionProps) | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | connectTo(driver, peerConnectionProps) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + driver | connection | query | operation | obfuscatedQuery + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3 from pg_user" | "SELECT" | "SELECT ? from pg_user" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | "SELECT 3" | "SELECT" | "SELECT ?" } def "statement update on #driver with #connection.getClass().getCanonicalName() generates a span"() { @@ -431,33 +671,85 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { then: def addDbmTag = dbmTraceInjected() statement.updateCount == 0 - assertTraces(1) { - trace(2) { - basicSpan(it, "parent") - span { - operationName this.operation(driver) - serviceName service(driver) - resourceName query - spanType DDSpanTypes.SQL - childOf span(0) - errored false - tags { - "$Tags.COMPONENT" "java-jdbc-statement" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT - "$Tags.DB_TYPE" driver - "$Tags.DB_INSTANCE" dbName.toLowerCase() - // only set when there is an out of proc instance (postgresql, mysql) - "$Tags.PEER_HOSTNAME" String - // currently there is a bug in the instrumentation with - // postgresql and mysql if the connection event is missed - // since Connection.getClientInfo will not provide the username - "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } - "${Tags.DB_OPERATION}" operation - if (addDbmTag) { - "$InstrumentationTags.DBM_TRACE_INJECTED" true + if (driver == POSTGRESQL || driver == MYSQL || !dbmTraceInjected()) { + assertTraces(1) { + trace(2) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName query + spanType DDSpanTypes.SQL + childOf span(0) + errored false + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "${Tags.DB_OPERATION}" operation + if (addDbmTag) { + "$InstrumentationTags.DBM_TRACE_INJECTED" true + } + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + } + } + } else { + assertTraces(1) { + trace(3) { + basicSpan(it, "parent") + span { + operationName this.operation(this.getDbType(driver)) + serviceName service(driver) + resourceName query + spanType DDSpanTypes.SQL + childOf span(0) + errored false + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + // only set when there is an out of proc instance (postgresql, mysql) + "$Tags.PEER_HOSTNAME" String + // currently there is a bug in the instrumentation with + // postgresql and mysql if the connection event is missed + // since Connection.getClientInfo will not provide the username + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "${Tags.DB_OPERATION}" operation + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() + } + } + span { + serviceName service(driver) + operationName this.operation(this.getDbType(driver)) + resourceName "set context_info ?" + spanType DDSpanTypes.SQL + childOf span(0) + errored false + measured true + tags { + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" this.getDbType(driver) + "$Tags.DB_INSTANCE" dbName.get(driver).toLowerCase() + "$Tags.PEER_HOSTNAME" String + "$Tags.DB_USER" { it == null || it == jdbcUserNames.get(driver) } + "$Tags.DB_OPERATION" "set" + "dd.instrumentation" true + peerServiceFrom(Tags.DB_INSTANCE) + defaultTags() } - peerServiceFrom(Tags.DB_INSTANCE) - defaultTags() } } } @@ -469,15 +761,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query | operation - "mysql" | connectTo(driver, peerConnectionProps) | "CREATE TEMPORARY TABLE s_test_ (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "postgresql" | connectTo(driver, peerConnectionProps) | "CREATE TEMPORARY TABLE s_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_tomcat_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_tomcat_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_hikari_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_hikari_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_c3p0_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_c3p0_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + driver | connection | query | operation + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "CREATE TEMPORARY TABLE s_test_ (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "CREATE TEMPORARY TABLE s_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | "CREATE TABLE #s_test_ (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_tomcat_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_tomcat_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | "CREATE TABLE #s_tomcat_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_hikari_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_hikari_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | "CREATE TABLE #s_hikari_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_c3p0_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "CREATE TEMPORARY TABLE s_c3p0_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | "CREATE TABLE #s_c3p0_test (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE" } @@ -501,15 +797,15 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "{ ? = call upper( ? ) }" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "{ ? = call upper( ? ) }" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | " { ? = call upper( ? ) }" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "{ ? = call upper( ? ) }" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | " { ? = call upper( ? ) }" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "{ ? = call upper( ? ) }" - "postgresql" | connectTo(driver, peerConnectionProps) | " { ? = call upper( ? ) }" - "mysql" | connectTo(driver, peerConnectionProps) | " { ? = call upper( ? ) }" + driver | connection | query + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "{ ? = call upper( ? ) }" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "{ ? = call upper( ? ) }" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "{ ? = call upper( ? ) }" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "{ ? = call upper( ? ) }" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "{ ? = call upper( ? ) }" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "{ ? = call upper( ? ) }" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "{ ? = call upper( ? ) }" + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "{ ? = call upper( ? ) }" } def "prepared procedure call on #driver with #connection.getClass().getCanonicalName() does not hang"() { @@ -533,6 +829,15 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { SELECT 1; END """ + } else if (driver == "sqlserver") { + createSql = + """ + CREATE PROCEDURE dummy @res integer output + AS + BEGIN + SELECT 1; + END + """ } else { assert false } @@ -542,8 +847,10 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { return } - - connection.prepareCall(createSql).execute() + // object already exists (no IF NOT EXISTS in SQL Server) + try { + connection.prepareCall(createSql).execute() + } catch (SQLServerException ex) {} injectSysConfig("dd.dbm.propagation.mode", "full") CallableStatement proc = connection.prepareCall(query) @@ -565,15 +872,19 @@ abstract class RemoteJDBCInstrumentationTest extends VersionedNamingTestBase { connection.close() where: - driver | connection | query - "postgresql" | cpDatasources.get("hikari").get(driver).getConnection() | "CALL dummy(?)" - "mysql" | cpDatasources.get("hikari").get(driver).getConnection() | "CALL dummy(?)" - "postgresql" | cpDatasources.get("tomcat").get(driver).getConnection() | " CALL dummy(?)" - "mysql" | cpDatasources.get("tomcat").get(driver).getConnection() | "CALL dummy(?)" - "postgresql" | cpDatasources.get("c3p0").get(driver).getConnection() | " CALL dummy(?)" - "mysql" | cpDatasources.get("c3p0").get(driver).getConnection() | "CALL dummy(?)" - "postgresql" | connectTo(driver, peerConnectionProps) | " CALL dummy(?)" - "mysql" | connectTo(driver, peerConnectionProps) | "CALL dummy(?)" + driver | connection | query + POSTGRESQL | cpDatasources.get("hikari").get(driver).getConnection() | "CALL dummy(?)" + MYSQL | cpDatasources.get("hikari").get(driver).getConnection() | "CALL dummy(?)" + SQLSERVER | cpDatasources.get("hikari").get(driver).getConnection() | "{CALL dummy(?)}" + POSTGRESQL | cpDatasources.get("tomcat").get(driver).getConnection() | "CALL dummy(?)" + MYSQL | cpDatasources.get("tomcat").get(driver).getConnection() | "CALL dummy(?)" + SQLSERVER | cpDatasources.get("tomcat").get(driver).getConnection() | "{CALL dummy(?)}" + POSTGRESQL | cpDatasources.get("c3p0").get(driver).getConnection() | "CALL dummy(?)" + MYSQL | cpDatasources.get("c3p0").get(driver).getConnection() | "CALL dummy(?)" + SQLSERVER | cpDatasources.get("c3p0").get(driver).getConnection() | "{CALL dummy(?)}" + POSTGRESQL | connectTo(driver, peerConnectionProps(driver)) | "CALL dummy(?)" + MYSQL | connectTo(driver, peerConnectionProps(driver)) | "CALL dummy(?)" + SQLSERVER | connectTo(driver, peerConnectionProps(driver)) | "{CALL dummy(?)}" } @@ -656,6 +967,12 @@ class RemoteJDBCInstrumentationV1ForkedTest extends RemoteJDBCInstrumentationTes protected boolean dbmTraceInjected() { return false } + + @Override + protected String getDbType(String dbType) { + final databaseNaming = new DatabaseNamingV1() + return databaseNaming.normalizedName(dbType) + } } class RemoteDBMTraceInjectedForkedTest extends RemoteJDBCInstrumentationTest { @@ -685,4 +1002,10 @@ class RemoteDBMTraceInjectedForkedTest extends RemoteJDBCInstrumentationTest { protected String operation(String dbType) { return "${dbType}.query" } + + @Override + protected String getDbType(String dbType) { + final databaseNaming = new DatabaseNamingV1() + return databaseNaming.normalizedName(dbType) + } } diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/SQLServerInjectionForkedTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/SQLServerInjectionForkedTest.groovy new file mode 100644 index 00000000000..2e958be9a3b --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/SQLServerInjectionForkedTest.groovy @@ -0,0 +1,36 @@ + + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.config.TraceInstrumentationConfig +import test.TestConnection +import test.TestDatabaseMetaData +import test.TestStatement + +class SQLServerInjectionForkedTest extends AgentTestRunner { + + @Override + void configurePreAgent() { + super.configurePreAgent() + + injectSysConfig(TraceInstrumentationConfig.DB_DBM_PROPAGATION_MODE_MODE, "full") + injectSysConfig("service.name", "my_service_name") + } + + static query = "SELECT 1" + static serviceInjection = "ddps='my_service_name',dddbs='sqlserver',ddh='localhost',dddb='testdb'" + + def "SQL Server no trace injection with full"() { + setup: + def connection = new TestConnection(false) + def metadata = new TestDatabaseMetaData() + metadata.setURL("jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=testdb;") + connection.setMetaData(metadata) + + when: + def statement = connection.createStatement() as TestStatement + statement.executeQuery(query) + + then: + assert statement.sql == "/*${serviceInjection}*/ ${query}" + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy index defb43d9792..9e75ef23d26 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy @@ -28,6 +28,7 @@ class TestConnection implements Connection { } } + private DatabaseMetaData metadata @Override Statement createStatement() throws SQLException { @@ -77,7 +78,14 @@ class TestConnection implements Connection { @Override DatabaseMetaData getMetaData() throws SQLException { - return new TestDatabaseMetaData() + if (metadata == null) { + return new TestDatabaseMetaData() + } + return metadata + } + + void setMetaData(DatabaseMetaData metadata) { + this.metadata = metadata } @Override diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy index 198f1832dd2..ce5b3e783c7 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy @@ -12,6 +12,8 @@ class TestDatabaseMetaData implements DatabaseMetaData { return false } + private String url + @Override boolean allTablesAreSelectable() throws SQLException { return false @@ -19,7 +21,14 @@ class TestDatabaseMetaData implements DatabaseMetaData { @Override String getURL() throws SQLException { - return "jdbc:testdb://localhost" + if (url == null) { + return "jdbc:testdb://localhost" + } + return url + } + + void setURL(String url) { + this.url = url } @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 8a2343522d8..8da026f224f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -1276,6 +1276,7 @@ public class CoreSpanBuilder implements AgentTracer.SpanBuilder { private Object builderRequestContextDataIast; private Object builderCiVisibilityContextData; private List links; + private long spanId; CoreSpanBuilder( final String instrumentationName, final CharSequence operationName, CoreTracer tracer) { @@ -1431,6 +1432,12 @@ public AgentTracer.SpanBuilder withLink(AgentSpanLink link) { return this; } + @Override + public CoreSpanBuilder withSpanId(final long spanId) { + this.spanId = spanId; + return this; + } + /** * Build the SpanContext, if the actual span has a parent, the following attributes must be * propagated: - ServiceName - Baggage - Trace (a list of all spans related) - SpanType @@ -1439,7 +1446,7 @@ public AgentTracer.SpanBuilder withLink(AgentSpanLink link) { */ private DDSpanContext buildSpanContext() { final DDTraceId traceId; - final long spanId = idGenerationStrategy.generateSpanId(); + final long spanId; final long parentSpanId; final Map baggage; final TraceCollector parentTraceCollector; @@ -1455,6 +1462,11 @@ private DDSpanContext buildSpanContext() { final PathwayContext pathwayContext; final PropagationTags propagationTags; + if (this.spanId == 0) { + spanId = idGenerationStrategy.generateSpanId(); + } else { + spanId = this.spanId; + } // FIXME [API] parentContext should be an interface implemented by ExtractedContext, // TagContext, DDSpanContext, AgentSpan.Context AgentSpan.Context parentContext = parent; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java index af9299df6e4..203eb664bd0 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java @@ -321,6 +321,8 @@ public interface SpanBuilder { SpanBuilder withRequestContextData(RequestContextSlot slot, T data); SpanBuilder withLink(AgentSpanLink link); + + SpanBuilder withSpanId(long spanId); } static class NoopTracerAPI implements TracerAPI {