diff --git a/.circleci/collect_reports.sh b/.circleci/collect_reports.sh index e813bbfb61..9cb27f8e94 100755 --- a/.circleci/collect_reports.sh +++ b/.circleci/collect_reports.sh @@ -4,6 +4,8 @@ # This folder will be saved by circleci and available after test runs. set -e +#Enable '**' support +shopt -s globstar REPORTS_DIR=./reports mkdir -p $REPORTS_DIR >/dev/null 2>&1 diff --git a/.circleci/collect_results.sh b/.circleci/collect_results.sh index 13072caf04..bde5432216 100755 --- a/.circleci/collect_results.sh +++ b/.circleci/collect_results.sh @@ -4,6 +4,8 @@ # This folder will be saved by circleci and available after test runs. set -e +#Enable '**' support +shopt -s globstar TEST_RESULTS_DIR=./results mkdir -p $TEST_RESULTS_DIR >/dev/null 2>&1 diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b3fb07018..8d85c6e741 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,6 +38,7 @@ jobs: - persist_to_workspace: root: . paths: + - .gradle - workspace - save_cache: @@ -93,7 +94,13 @@ jobs: <<: *default_test_job environment: # We are building on Java8, this is our default JVM so no need to set more homes - - TEST_TASK: test latestDepTest jacocoTestReport jacocoTestCoverageVerification + - TEST_TASK: test jacocoTestReport jacocoTestCoverageVerification + + test_latest8: + <<: *default_test_job + environment: + # We are building on Java8, this is our default JVM so no need to set more homes + - TEST_TASK: latestDepTest test_ibm8: <<: *default_test_job @@ -249,6 +256,12 @@ workflows: filters: tags: only: /.*/ + - test_latest8: + requires: + - build + filters: + tags: + only: /.*/ - test_ibm8: requires: - build @@ -305,6 +318,7 @@ workflows: requires: - test_7 - test_8 + - test_latest8 - test_ibm8 - test_9 - test_10 @@ -322,6 +336,7 @@ workflows: requires: - test_7 - test_8 + - test_latest8 - test_ibm8 - test_9 - test_10 diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/decorator/DatabaseClientDecorator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/decorator/DatabaseClientDecorator.java index edbdda6c4a..14995cd19e 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/decorator/DatabaseClientDecorator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/decorator/DatabaseClientDecorator.java @@ -1,5 +1,7 @@ package datadog.trace.agent.decorator; +import datadog.trace.api.Config; +import datadog.trace.api.DDTags; import io.opentracing.Span; import io.opentracing.tag.Tags; @@ -29,7 +31,11 @@ public abstract class DatabaseClientDecorator extends ClientDecorato assert span != null; if (connection != null) { Tags.DB_USER.set(span, dbUser(connection)); - Tags.DB_INSTANCE.set(span, dbInstance(connection)); + final String instanceName = dbInstance(connection); + Tags.DB_INSTANCE.set(span, instanceName); + if (instanceName != null && Config.get().isDbClientSplitByInstance()) { + span.setTag(DDTags.SERVICE_NAME, instanceName); + } } return span; } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java index 52289b56ff..6899cfeaba 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java @@ -260,7 +260,10 @@ public class ReferenceCreator extends ClassVisitor { // * DONE field-source class (descriptor) // * DONE field-source visibility from this point (PRIVATE?) - final Type ownerType = Type.getType("L" + owner + ";"); + final Type ownerType = + owner.startsWith("[") + ? underlyingType(Type.getType(owner)) + : Type.getType("L" + owner + ";"); final Type fieldType = Type.getType(descriptor); final List fieldFlags = new ArrayList<>(); @@ -334,7 +337,10 @@ public class ReferenceCreator extends ClassVisitor { } } - final Type ownerType = Type.getType("L" + owner + ";"); + final Type ownerType = + owner.startsWith("[") + ? underlyingType(Type.getType(owner)) + : Type.getType("L" + owner + ";"); final List methodFlags = new ArrayList<>(); methodFlags.add( diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/DatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/DatabaseClientDecoratorTest.groovy index e1bae7ae91..b37c5c8680 100644 --- a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/DatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/DatabaseClientDecoratorTest.groovy @@ -1,9 +1,12 @@ package datadog.trace.agent.decorator +import datadog.trace.api.Config import datadog.trace.api.DDTags import io.opentracing.Span import io.opentracing.tag.Tags +import static datadog.trace.agent.test.utils.ConfigUtils.withConfigOverride + class DatabaseClientDecoratorTest extends ClientDecoratorTest { def span = Mock(Span) @@ -35,21 +38,26 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest { def decorator = newDecorator() when: - decorator.onConnection(span, session) + withConfigOverride(Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "$renameService") { + decorator.onConnection(span, session) + } then: if (session) { 1 * span.setTag(Tags.DB_USER.key, session.user) 1 * span.setTag(Tags.DB_INSTANCE.key, session.instance) + if (renameService && session.instance) { + 1 * span.setTag(DDTags.SERVICE_NAME, session.instance) + } } 0 * _ where: - session | _ - null | _ - [user: "test-user"] | _ - [instance: "test-instance"] | _ - [user: "test-user", instance: "test-instance"] | _ + renameService | session + false | null + true | [user: "test-user"] + false | [instance: "test-instance"] + true | [user: "test-user", instance: "test-instance"] } def "test onStatement"() { diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/HttpClientDecoratorTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/HttpClientDecoratorTest.groovy index f9ed2a9189..afab046789 100644 --- a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/HttpClientDecoratorTest.groovy +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/HttpClientDecoratorTest.groovy @@ -26,12 +26,12 @@ class HttpClientDecoratorTest extends ClientDecoratorTest { then: if (req) { - 1 * span.setTag(Tags.HTTP_METHOD.key, "test-method") - 1 * span.setTag(Tags.HTTP_URL.key, "$testUrl") - 1 * span.setTag(Tags.PEER_HOSTNAME.key, "test-host") - 1 * span.setTag(Tags.PEER_PORT.key, 555) + 1 * span.setTag(Tags.HTTP_METHOD.key, req.method) + 1 * span.setTag(Tags.HTTP_URL.key, "$req.url") + 1 * span.setTag(Tags.PEER_HOSTNAME.key, req.host) + 1 * span.setTag(Tags.PEER_PORT.key, req.port) if (renameService) { - 1 * span.setTag(DDTags.SERVICE_NAME, "test-host") + 1 * span.setTag(DDTags.SERVICE_NAME, req.host) } } 0 * _ diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/OrmClientDecoratorTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/OrmClientDecoratorTest.groovy index 4e99cb5fc0..810b51519e 100644 --- a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/OrmClientDecoratorTest.groovy +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/decorator/OrmClientDecoratorTest.groovy @@ -1,12 +1,9 @@ package datadog.trace.agent.decorator import datadog.trace.api.DDTags -import io.opentracing.Span class OrmClientDecoratorTest extends DatabaseClientDecoratorTest { - def span = Mock(Span) - def "test onOperation #testName"() { setup: decorator = newDecorator({ e -> entityName }) diff --git a/dd-java-agent/dd-java-agent.gradle b/dd-java-agent/dd-java-agent.gradle index 81ccc6669a..f408630093 100644 --- a/dd-java-agent/dd-java-agent.gradle +++ b/dd-java-agent/dd-java-agent.gradle @@ -97,13 +97,6 @@ dependencies { testCompile deps.opentracingMock testCompile deps.testLogging testCompile deps.guava - testCompile group: 'org.assertj', name: 'assertj-core', version: '2.9.+' - testCompile group: 'org.mockito', name: 'mockito-core', version: '2.19.0' - - testCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.4.2' - testCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.4.2' - // run embedded mongodb for integration testing - testCompile group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5' } tasks.withType(Test).configureEach { diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy b/dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy index ff13ad89ca..3ae9770f32 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy @@ -23,8 +23,12 @@ class AkkaHttpClientInstrumentationTest extends HttpClientTest { public static final CassandraClientDecorator DECORATE = new CassandraClientDecorator(); @@ -53,15 +50,7 @@ public class CassandraClientDecorator extends DatabaseClientDecorator { if (result != null) { final Host host = result.getExecutionInfo().getQueriedHost(); Tags.PEER_PORT.set(span, host.getSocketAddress().getPort()); - Tags.PEER_HOSTNAME.set(span, host.getAddress().getHostName()); - - final InetAddress inetAddress = host.getSocketAddress().getAddress(); - if (inetAddress instanceof Inet4Address) { - final byte[] address = inetAddress.getAddress(); - Tags.PEER_HOST_IPV4.set(span, ByteBuffer.wrap(address).getInt()); - } else { - Tags.PEER_HOST_IPV6.set(span, inetAddress.getHostAddress()); - } + onPeerConnection(span, host.getSocketAddress().getAddress()); } return span; } diff --git a/dd-java-agent/instrumentation/datastax-cassandra-3/src/test/groovy/CassandraClientTest.groovy b/dd-java-agent/instrumentation/datastax-cassandra-3/src/test/groovy/CassandraClientTest.groovy index 14c55c407c..18968c1ea1 100644 --- a/dd-java-agent/instrumentation/datastax-cassandra-3/src/test/groovy/CassandraClientTest.groovy +++ b/dd-java-agent/instrumentation/datastax-cassandra-3/src/test/groovy/CassandraClientTest.groovy @@ -2,15 +2,21 @@ import com.datastax.driver.core.Cluster import com.datastax.driver.core.Session import datadog.opentracing.DDSpan import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.api.DDSpanTypes import io.opentracing.tag.Tags import org.cassandraunit.utils.EmbeddedCassandraServerHelper import spock.lang.Shared +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + class CassandraClientTest extends AgentTestRunner { @Shared Cluster cluster + @Shared + int port = 9142 def setupSpec() { /* @@ -34,72 +40,91 @@ class CassandraClientTest extends AgentTestRunner { EmbeddedCassandraServerHelper.cleanEmbeddedCassandra() } - def "sync traces"() { + def "test sync"() { setup: - final Session session = cluster.newSession() + Session session = cluster.connect(keyspace) - session.execute("DROP KEYSPACE IF EXISTS sync_test") - session.execute( - "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}") - session.execute("CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )") - session.execute("INSERT INTO sync_test.users (id, name) values (uuid(), 'alice')") - session.execute("SELECT * FROM sync_test.users where name = 'alice' ALLOW FILTERING") - - def query = "SELECT * FROM sync_test.users where name = 'alice' ALLOW FILTERING" + session.execute(statement) expect: - session.getClass().getName().endsWith("cassandra.TracingSession") - TEST_WRITER.size() == 5 - final DDSpan selectTrace = TEST_WRITER.get(TEST_WRITER.size() - 1).get(0) + assertTraces(keyspace ? 2 : 1) { + if (keyspace) { + trace(0, 1) { + cassandraSpan(it, 0, "USE $keyspace", null) + } + } + trace(keyspace ? 1 : 0, 1) { + cassandraSpan(it, 0, statement, keyspace) + } + } - selectTrace.getServiceName() == "cassandra" - selectTrace.getOperationName() == "cassandra.query" - selectTrace.getResourceName() == query - selectTrace.getSpanType() == DDSpanTypes.CASSANDRA + cleanup: + session.close() - selectTrace.getTags().get(Tags.COMPONENT.getKey()) == "java-cassandra" - selectTrace.getTags().get(Tags.DB_TYPE.getKey()) == "cassandra" - selectTrace.getTags().get(Tags.PEER_HOSTNAME.getKey()) == "localhost" - // More info about IPv4 tag: https://trello.com/c/2el2IwkF/174-mongodb-ot-contrib-provides-a-wrong-peeripv4 - selectTrace.getTags().get(Tags.PEER_HOST_IPV4.getKey()) == 2130706433 - selectTrace.getTags().get(Tags.PEER_PORT.getKey()) == 9142 - selectTrace.getTags().get(Tags.SPAN_KIND.getKey()) == "client" + where: + statement | keyspace + "DROP KEYSPACE IF EXISTS sync_test" | null + "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | null + "CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )" | "sync_test" + "INSERT INTO sync_test.users (id, name) values (uuid(), 'alice')" | "sync_test" + "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "sync_test" } - def "async traces"() { + def "test async"() { setup: - final Session session = cluster.connectAsync().get() - - session.executeAsync("DROP KEYSPACE IF EXISTS async_test").get() - session - .executeAsync( - "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}") - .get() - session.executeAsync("CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )").get() - session.executeAsync("INSERT INTO async_test.users (id, name) values (uuid(), 'alice')").get() - TEST_WRITER.waitForTraces(4) - session - .executeAsync("SELECT * FROM async_test.users where name = 'alice' ALLOW FILTERING") - .get() - TEST_WRITER.waitForTraces(5) - - def query = "SELECT * FROM async_test.users where name = 'alice' ALLOW FILTERING" + Session session = cluster.connect(keyspace) + runUnderTrace("parent") { + session.executeAsync(statement) + blockUntilChildSpansFinished(1) + } expect: - session.getClass().getName().endsWith("cassandra.TracingSession") - final DDSpan selectTrace = TEST_WRITER.get(TEST_WRITER.size() - 1).get(0) + assertTraces(keyspace ? 2 : 1) { + if (keyspace) { + trace(0, 1) { + cassandraSpan(it, 0, "USE $keyspace", null) + } + } + trace(keyspace ? 1 : 0, 2) { + basicSpan(it, 0, "parent") + cassandraSpan(it, 1, statement, keyspace, span(0)) + } + } - selectTrace.getServiceName() == "cassandra" - selectTrace.getOperationName() == "cassandra.query" - selectTrace.getResourceName() == query - selectTrace.getSpanType() == DDSpanTypes.CASSANDRA + cleanup: + session.close() - selectTrace.getTags().get(Tags.COMPONENT.getKey()) == "java-cassandra" - selectTrace.getTags().get(Tags.DB_TYPE.getKey()) == "cassandra" - selectTrace.getTags().get(Tags.PEER_HOSTNAME.getKey()) == "localhost" - // More info about IPv4 tag: https://trello.com/c/2el2IwkF/174-mongodb-ot-contrib-provides-a-wrong-peeripv4 - selectTrace.getTags().get(Tags.PEER_HOST_IPV4.getKey()) == 2130706433 - selectTrace.getTags().get(Tags.PEER_PORT.getKey()) == 9142 - selectTrace.getTags().get(Tags.SPAN_KIND.getKey()) == "client" + where: + statement | keyspace + "DROP KEYSPACE IF EXISTS async_test" | null + "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | null + "CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )" | "async_test" + "INSERT INTO async_test.users (id, name) values (uuid(), 'alice')" | "async_test" + "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "async_test" } + + def cassandraSpan(TraceAssert trace, int index, String statement, String keyspace, Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + serviceName "cassandra" + operationName "cassandra.query" + resourceName statement + spanType DDSpanTypes.CASSANDRA + if (parentSpan == null) { + parent() + } else { + childOf((DDSpan) parentSpan) + } + tags { + "$Tags.COMPONENT.key" "java-cassandra" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "$Tags.DB_INSTANCE.key" keyspace + "$Tags.DB_TYPE.key" "cassandra" + "$Tags.PEER_HOSTNAME.key" "localhost" + "$Tags.PEER_HOST_IPV4.key" "127.0.0.1" + "$Tags.PEER_PORT.key" port + defaultTags() + } + } + } + } diff --git a/dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/SpringJpaTest.groovy b/dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/SpringJpaTest.groovy index b936663e61..6a33ef9b3d 100644 --- a/dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/SpringJpaTest.groovy +++ b/dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/SpringJpaTest.groovy @@ -26,16 +26,12 @@ class SpringJpaTest extends AgentTestRunner { !repo.findAll().iterator().hasNext() assertTraces(1) { - trace(0, 2) { + trace(0, 1) { span(0) { serviceName "hsqldb" + resourceName "select customer0_.id as id1_0_, customer0_.firstName as firstNam2_0_, customer0_.lastName as lastName3_0_ from Customer customer0_" spanType "sql" } - span(1) { - serviceName "hsqldb" - spanType "sql" - childOf(span(0)) - } } } TEST_WRITER.clear() @@ -49,25 +45,22 @@ class SpringJpaTest extends AgentTestRunner { // Behavior changed in new version: def extraTrace = TEST_WRITER.size() == 2 assertTraces(extraTrace ? 2 : 1) { - trace(0, 2) { - span(0) { - serviceName "hsqldb" - spanType "sql" - } - span(1) { - serviceName "hsqldb" - spanType "sql" - childOf(span(0)) - } - } if (extraTrace) { - trace(1, 1) { + trace(0, 1) { span(0) { serviceName "hsqldb" + resourceName "call next value for hibernate_sequence" spanType "sql" } } } + trace(extraTrace ? 1 : 0, 1) { + span(0) { + serviceName "hsqldb" + resourceName ~/insert into Customer \(.*\) values \(.*, \?, \?\)/ + spanType "sql" + } + } } TEST_WRITER.clear() @@ -78,20 +71,17 @@ class SpringJpaTest extends AgentTestRunner { then: customer.id == savedId assertTraces(2) { - trace(0, 2) { + trace(0, 1) { span(0) { serviceName "hsqldb" + resourceName "select customer0_.id as id1_0_0_, customer0_.firstName as firstNam2_0_0_, customer0_.lastName as lastName3_0_0_ from Customer customer0_ where customer0_.id=?" spanType "sql" } - span(1) { - serviceName "hsqldb" - spanType "sql" - childOf(span(0)) - } } trace(1, 1) { span(0) { serviceName "hsqldb" + resourceName "update Customer set firstName=?, lastName=? where id=?" spanType "sql" } } @@ -105,16 +95,12 @@ class SpringJpaTest extends AgentTestRunner { customer.id == savedId customer.firstName == "Bill" assertTraces(1) { - trace(0, 2) { + trace(0, 1) { span(0) { serviceName "hsqldb" + resourceName "select customer0_.id as id1_0_, customer0_.firstName as firstNam2_0_, customer0_.lastName as lastName3_0_ from Customer customer0_ where customer0_.lastName=?" spanType "sql" } - span(1) { - serviceName "hsqldb" - spanType "sql" - childOf(span(0)) - } } } TEST_WRITER.clear() @@ -124,20 +110,17 @@ class SpringJpaTest extends AgentTestRunner { then: assertTraces(2) { - trace(0, 2) { + trace(0, 1) { span(0) { serviceName "hsqldb" + resourceName "select customer0_.id as id1_0_0_, customer0_.firstName as firstNam2_0_0_, customer0_.lastName as lastName3_0_0_ from Customer customer0_ where customer0_.id=?" spanType "sql" } - span(1) { - serviceName "hsqldb" - spanType "sql" - childOf(span(0)) - } } trace(1, 1) { span(0) { serviceName "hsqldb" + resourceName "delete from Customer where id=?" spanType "sql" } } diff --git a/dd-java-agent/instrumentation/java-concurrent/src/slickTest/groovy/SlickTest.groovy b/dd-java-agent/instrumentation/java-concurrent/src/slickTest/groovy/SlickTest.groovy index 99f16f99ae..6bd75f2787 100644 --- a/dd-java-agent/instrumentation/java-concurrent/src/slickTest/groovy/SlickTest.groovy +++ b/dd-java-agent/instrumentation/java-concurrent/src/slickTest/groovy/SlickTest.groovy @@ -42,7 +42,7 @@ class SlickTest extends AgentTestRunner { "$Tags.DB_TYPE.key" SlickUtils.Driver() "$Tags.DB_USER.key" SlickUtils.Username() - "db.instance" SlickUtils.Url() + "db.instance" SlickUtils.Db() "span.origin.type" "org.h2.jdbc.JdbcPreparedStatement" defaultTags() diff --git a/dd-java-agent/instrumentation/java-concurrent/src/slickTest/scala/SlickUtils.scala b/dd-java-agent/instrumentation/java-concurrent/src/slickTest/scala/SlickUtils.scala index c1cb3514f0..1bc64c19e9 100644 --- a/dd-java-agent/instrumentation/java-concurrent/src/slickTest/scala/SlickUtils.scala +++ b/dd-java-agent/instrumentation/java-concurrent/src/slickTest/scala/SlickUtils.scala @@ -35,8 +35,9 @@ class SlickUtils { object SlickUtils { val Driver = "h2" + val Db = "test" val Username = "TESTUSER" - val Url = s"jdbc:${Driver}:mem:test" + val Url = s"jdbc:${Driver}:mem:${Db}" val TestValue = 3 val TestQuery = "SELECT 3" diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DBInfo.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DBInfo.java new file mode 100644 index 0000000000..d59e343c72 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DBInfo.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.jdbc; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder(builderClassName = "Builder", toBuilder = true) +public class DBInfo { + public static DBInfo DEFAULT = new Builder().type("database").build(); + private final String type; + private final String subtype; + private final String url; + private final String user; + private final String instance; + private final String db; + private final String host; + private final Integer port; +} diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DriverInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DriverInstrumentation.java new file mode 100644 index 0000000000..a5340ddbd3 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/DriverInstrumentation.java @@ -0,0 +1,71 @@ +package datadog.trace.instrumentation.jdbc; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +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 DriverInstrumentation extends Instrumenter.Default { + + public DriverInstrumentation() { + super("jdbc"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("java.sql.Driver"))); + } + + @Override + public String[] helperClassNames() { + final List helpers = new ArrayList<>(JDBCConnectionUrlParser.values().length + 4); + + helpers.add(packageName + ".DBInfo"); + helpers.add(packageName + ".DBInfo$Builder"); + helpers.add(packageName + ".JDBCMaps"); + helpers.add(packageName + ".JDBCConnectionUrlParser"); + + for (final JDBCConnectionUrlParser parser : JDBCConnectionUrlParser.values()) { + helpers.add(parser.getClass().getName()); + } + return helpers.toArray(new String[0]); + } + + @Override + public Map, String> transformers() { + return singletonMap( + nameStartsWith("connect") + .and(takesArgument(0, String.class)) + .and(takesArgument(1, Properties.class)) + .and(returns(Connection.class)), + DriverAdvice.class.getName()); + } + + public static class DriverAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addDBInfo( + @Advice.Argument(0) final String url, + @Advice.Argument(1) final Properties props, + @Advice.Return final Connection connection) { + final DBInfo dbInfo = JDBCConnectionUrlParser.parse(url, props); + JDBCMaps.connectionInfo.put(connection, dbInfo); + } + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCConnectionUrlParser.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCConnectionUrlParser.java new file mode 100644 index 0000000000..8f5124ad79 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCConnectionUrlParser.java @@ -0,0 +1,802 @@ +package datadog.trace.instrumentation.jdbc; + +import static datadog.trace.instrumentation.jdbc.DBInfo.DEFAULT; + +import datadog.trace.bootstrap.ExceptionLogger; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Structured as an enum instead of a class hierarchy to allow iterating through the parsers + * automatically without having to maintain a separate list of parsers. + */ +public enum JDBCConnectionUrlParser { + GENERIC_URL_LIKE() { + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + try { + // Attempt generic parsing + final URI uri = new URI(jdbcUrl); + + populateStandardProperties(builder, splitQuery(uri.getQuery(), "&")); + + final String user = uri.getUserInfo(); + if (user != null) { + builder.user(user); + } + + String path = uri.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (!path.isEmpty()) { + builder.db(path); + } + + if (uri.getHost() != null) { + builder.host(uri.getHost()); + } + + if (uri.getPort() > 0) { + builder.port(uri.getPort()); + } + + return builder.type(uri.getScheme()); + } catch (final Exception e) { + return builder; + } + } + }, + + MODIFIED_URL_LIKE() { + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final String type; + String serverName = ""; + Integer port = null; + String instanceName = null; + final String user = null; + + final int hostIndex = jdbcUrl.indexOf("://"); + + if (hostIndex <= 0) { + return builder; + } + + type = jdbcUrl.substring(0, hostIndex); + + final String[] split; + if (type.equals("db2") || type.equals("as400")) { + if (jdbcUrl.contains("=")) { + final int paramLoc = jdbcUrl.lastIndexOf(":"); + split = new String[] {jdbcUrl.substring(0, paramLoc), jdbcUrl.substring(paramLoc + 1)}; + } else { + split = new String[] {jdbcUrl}; + } + } else { + split = jdbcUrl.split(";", 2); + } + + if (split.length > 1) { + final Map props = splitQuery(split[1], ";"); + populateStandardProperties(builder, props); + if (props.containsKey("servername")) { + serverName = props.get("servername"); + } + } + + final String urlServerName = split[0].substring(hostIndex + 3); + if (!urlServerName.isEmpty()) { + serverName = urlServerName; + } + + int instanceLoc = serverName.indexOf("/"); + if (instanceLoc > 1) { + instanceName = serverName.substring(instanceLoc + 1); + serverName = serverName.substring(0, instanceLoc); + } + + final int portLoc = serverName.indexOf(":"); + + if (portLoc > 1) { + port = Integer.parseInt(serverName.substring(portLoc + 1)); + serverName = serverName.substring(0, portLoc); + } + + instanceLoc = serverName.indexOf("\\"); + if (instanceLoc > 1) { + instanceName = serverName.substring(instanceLoc + 1); + serverName = serverName.substring(0, instanceLoc); + } + + if (instanceName != null) { + builder.instance(instanceName); + } + + if (!serverName.isEmpty()) { + builder.host(serverName); + } + + if (port != null) { + builder.port(port); + } + + if (user != null) { + builder.user(user); + } + + return builder.type(type); + } + }, + + POSTGRES("postgresql") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 5432; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + MYSQL("mysql", "mariadb") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 3306; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + final int protoLoc = jdbcUrl.indexOf("://"); + final int typeEndLoc = dbInfo.getType().length(); + if (protoLoc > typeEndLoc) { + return MARIA_SUBPROTO + .doParse(jdbcUrl.substring(protoLoc + 3), builder) + .subtype(jdbcUrl.substring(typeEndLoc + 1, protoLoc)); + } + if (protoLoc > 0) { + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + + final int hostEndLoc; + final int portLoc = jdbcUrl.indexOf(":", typeEndLoc + 1); + final int dbLoc = jdbcUrl.indexOf("/", typeEndLoc); + final int paramLoc = jdbcUrl.indexOf("?", dbLoc); + + if (paramLoc > 0) { + populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); + builder.db(jdbcUrl.substring(dbLoc + 1, paramLoc)); + } else { + builder.db(jdbcUrl.substring(dbLoc + 1)); + } + + if (portLoc > 0) { + hostEndLoc = portLoc; + try { + builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, dbLoc))); + } catch (final NumberFormatException e) { + } + } else { + hostEndLoc = dbLoc; + } + + builder.host(jdbcUrl.substring(typeEndLoc + 1, hostEndLoc)); + + return builder; + } + }, + + MARIA_SUBPROTO() { + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final int hostEndLoc; + final int clusterSepLoc = jdbcUrl.indexOf(","); + final int ipv6End = jdbcUrl.startsWith("[") ? jdbcUrl.indexOf("]") : -1; + int portLoc = jdbcUrl.indexOf(":", Math.max(0, ipv6End)); + portLoc = clusterSepLoc < portLoc ? -1 : portLoc; + final int dbLoc = jdbcUrl.indexOf("/", Math.max(portLoc, clusterSepLoc)); + + final int paramLoc = jdbcUrl.indexOf("?", dbLoc); + + if (paramLoc > 0) { + populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); + builder.db(jdbcUrl.substring(dbLoc + 1, paramLoc)); + } else { + builder.db(jdbcUrl.substring(dbLoc + 1)); + } + + if (jdbcUrl.startsWith("address=")) { + return MARIA_ADDRESS.doParse(jdbcUrl, builder); + } + + if (portLoc > 0) { + hostEndLoc = portLoc; + final int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; + try { + builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, portEndLoc))); + } catch (final NumberFormatException e) { + } + } else { + hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; + } + + if (ipv6End > 0) { + builder.host(jdbcUrl.substring(1, ipv6End)); + } else { + builder.host(jdbcUrl.substring(0, hostEndLoc)); + } + return builder; + } + }, + + MARIA_ADDRESS() { + private final Pattern HOST_REGEX = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private final Pattern PORT_REGEX = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private final Pattern USER_REGEX = Pattern.compile("\\(\\s*user\\s*=\\s*([^ )]+)\\s*\\)"); + + @Override + DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { + final int addressEnd = jdbcUrl.indexOf(",address="); + if (addressEnd > 0) { + jdbcUrl = jdbcUrl.substring(0, addressEnd); + } + final Matcher hostMatcher = HOST_REGEX.matcher(jdbcUrl); + if (hostMatcher.find()) { + builder.host(hostMatcher.group(1)); + } + + final Matcher portMatcher = PORT_REGEX.matcher(jdbcUrl); + if (portMatcher.find()) { + builder.port(Integer.parseInt(portMatcher.group(1))); + } + + final Matcher userMatcher = USER_REGEX.matcher(jdbcUrl); + if (userMatcher.find()) { + builder.user(userMatcher.group(1)); + } + + return builder; + } + }, + + SAP("sap") { + private static final String DEFAULT_HOST = "localhost"; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + MSSQLSERVER("microsoft", "sqlserver") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 1433; + private static final String DEFAULT_INSTANCE = "MSSQLSERVER"; + + @Override + DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { + if (jdbcUrl.startsWith("microsoft:")) { + jdbcUrl = jdbcUrl.substring("microsoft:".length()); + } + if (!jdbcUrl.startsWith("sqlserver://")) { + return builder; + } + builder.type("sqlserver"); + final DBInfo dbInfo = builder.build(); + if (dbInfo.getInstance() == null) { + builder.instance(DEFAULT_INSTANCE); + } + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + DB2("db2", "as400") { + private static final int DEFAULT_PORT = 50000; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + ORACLE("oracle") { + private static final int DEFAULT_PORT = 1521; + + @Override + DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { + final int typeEndIndex = jdbcUrl.indexOf(":", "oracle:".length()); + final String subtype = jdbcUrl.substring("oracle:".length(), typeEndIndex); + jdbcUrl = jdbcUrl.substring(typeEndIndex + 1); + + builder.subtype(subtype); + final DBInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + + if (jdbcUrl.contains("@")) { + return ORACLE_AT.doParse(jdbcUrl, builder); + } else { + return ORACLE_CONNECT_INFO.doParse(jdbcUrl, builder); + } + } + }, + + ORACLE_CONNECT_INFO() { + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + + final String host; + final Integer port; + final String instance; + + final int hostEnd = jdbcUrl.indexOf(":"); + final int instanceLoc = jdbcUrl.indexOf("/"); + if (hostEnd > 0) { + host = jdbcUrl.substring(0, hostEnd); + final int afterHostEnd = jdbcUrl.indexOf(":", hostEnd + 1); + if (afterHostEnd > 0) { + port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, afterHostEnd)); + instance = jdbcUrl.substring(afterHostEnd + 1); + } else { + if (instanceLoc > 0) { + instance = jdbcUrl.substring(instanceLoc + 1); + port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, instanceLoc)); + } else { + final String portOrInstance = jdbcUrl.substring(hostEnd + 1); + Integer parsedPort = null; + try { + parsedPort = Integer.parseInt(portOrInstance); + } catch (final NumberFormatException e) { + } + if (parsedPort == null) { + port = null; + instance = portOrInstance; + } else { + port = parsedPort; + instance = null; + } + } + } + } else { + if (instanceLoc > 0) { + host = jdbcUrl.substring(0, instanceLoc); + port = null; + instance = jdbcUrl.substring(instanceLoc + 1); + } else { + if (jdbcUrl.isEmpty()) { + return builder; + } else { + host = null; + port = null; + instance = jdbcUrl; + } + } + } + if (host != null) { + builder.host(host); + } + if (port != null) { + builder.port(port); + } + return builder.instance(instance); + } + }, + + ORACLE_AT() { + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + if (jdbcUrl.contains("@(description")) { + return ORACLE_AT_DESCRIPTION.doParse(jdbcUrl, builder); + } + final String user; + + final String[] atSplit = jdbcUrl.split("@", 2); + + final int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + user = atSplit[0].substring(0, userInfoLoc); + } else { + user = null; + } + + final String connectInfo = atSplit[1]; + final int hostStart; + if (connectInfo.startsWith("//")) { + hostStart = "//".length(); + } else if (connectInfo.startsWith("ldap://")) { + hostStart = "ldap://".length(); + } else { + hostStart = 0; + } + if (user != null) { + builder.user(user); + } + return ORACLE_CONNECT_INFO.doParse(connectInfo.substring(hostStart), builder); + } + }, + + /** + * This parser can locate incorrect data if multiple addresses are defined but not everything is + * defined in the first block. (It would locate data from subsequent address blocks. + */ + ORACLE_AT_DESCRIPTION() { + private final Pattern HOST_REGEX = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private final Pattern PORT_REGEX = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private final Pattern INSTANCE_REGEX = + Pattern.compile("\\(\\s*service_name\\s*=\\s*([^ )]+)\\s*\\)"); + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final String[] atSplit = jdbcUrl.split("@", 2); + + final int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + builder.user(atSplit[0].substring(0, userInfoLoc)); + } + + final Matcher hostMatcher = HOST_REGEX.matcher(atSplit[1]); + if (hostMatcher.find()) { + builder.host(hostMatcher.group(1)); + } + + final Matcher portMatcher = PORT_REGEX.matcher(atSplit[1]); + if (portMatcher.find()) { + builder.port(Integer.parseInt(portMatcher.group(1))); + } + + final Matcher instanceMatcher = INSTANCE_REGEX.matcher(atSplit[1]); + if (instanceMatcher.find()) { + builder.instance(instanceMatcher.group(1)); + } + + return builder; + } + }, + + H2("h2") { + private static final int DEFAULT_PORT = 8082; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + final String instance; + + final String h2Url = jdbcUrl.substring("h2:".length()); + if (h2Url.startsWith("mem:")) { + builder.subtype("mem"); + final int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("mem:".length(), propLoc); + } else { + instance = h2Url.substring("mem:".length()); + } + } else if (h2Url.startsWith("file:")) { + builder.subtype("file"); + final int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("file:".length(), propLoc); + } else { + instance = h2Url.substring("file:".length()); + } + } else if (h2Url.startsWith("zip:")) { + builder.subtype("zip"); + final int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("zip:".length(), propLoc); + } else { + instance = h2Url.substring("zip:".length()); + } + } else if (h2Url.startsWith("tcp:")) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("h2").subtype("tcp"); + } else if (h2Url.startsWith("ssl:")) { + final DBInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("h2").subtype("ssl"); + } else { + builder.subtype("file"); + final int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring(0, propLoc); + } else { + instance = h2Url; + } + } + if (!instance.isEmpty()) { + builder.instance(instance); + } + return builder; + } + }, + + HSQL("hsqldb") { + private static final String DEFAULT_USER = "SA"; + private static final int DEFAULT_PORT = 9001; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + String instance = null; + final DBInfo dbInfo = builder.build(); + if (dbInfo.getUser() == null) { + builder.user(DEFAULT_USER); + } + final String hsqlUrl = jdbcUrl.substring("hsqldb:".length()); + if (hsqlUrl.startsWith("mem:")) { + builder.subtype("mem"); + instance = hsqlUrl.substring("mem:".length()); + } else if (hsqlUrl.startsWith("file:")) { + builder.subtype("file"); + instance = hsqlUrl.substring("file:".length()); + } else if (hsqlUrl.startsWith("res:")) { + builder.subtype("res"); + instance = hsqlUrl.substring("res:".length()); + } else if (hsqlUrl.startsWith("hsql:")) { + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("hsqldb").subtype("hsql"); + } else if (hsqlUrl.startsWith("hsqls:")) { + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("hsqldb").subtype("hsqls"); + } else if (hsqlUrl.startsWith("http:")) { + if (dbInfo.getPort() == null) { + builder.port(80); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("hsqldb").subtype("http"); + } else if (hsqlUrl.startsWith("https:")) { + if (dbInfo.getPort() == null) { + builder.port(443); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).type("hsqldb").subtype("https"); + } else { + builder.subtype("mem"); + instance = hsqlUrl; + } + return builder.instance(instance); + } + }, + + DERBY("derby") { + private static final String DEFAULT_USER = "APP"; + private static final int DEFAULT_PORT = 1527; + + @Override + DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + String instance = null; + String host = null; + + final DBInfo dbInfo = builder.build(); + if (dbInfo.getUser() == null) { + builder.user(DEFAULT_USER); + } + + final String derbyUrl = jdbcUrl.substring("derby:".length()); + final String[] split = derbyUrl.split(";", 2); + + if (split.length > 1) { + populateStandardProperties(builder, splitQuery(split[1], ";")); + } + + final String details = split[0]; + if (details.startsWith("memory:")) { + builder.subtype("memory"); + final String urlInstance = details.substring("memory:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("directory:")) { + builder.subtype("directory"); + final String urlInstance = details.substring("directory:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("classpath:")) { + builder.subtype("classpath"); + final String urlInstance = details.substring("classpath:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("jar:")) { + builder.subtype("jar"); + final String urlInstance = details.substring("jar:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("//")) { + builder.subtype("network"); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + String url = details.substring("//".length()); + final int instanceLoc = url.indexOf("/"); + if (instanceLoc >= 0) { + instance = url.substring(instanceLoc + 1); + final int protoLoc = instance.indexOf(":"); + if (protoLoc >= 0) { + instance = instance.substring(protoLoc + 1); + } + url = url.substring(0, instanceLoc); + } + final int portLoc = url.indexOf(":"); + if (portLoc > 0) { + host = url.substring(0, portLoc); + builder.port(Integer.parseInt(url.substring(portLoc + 1))); + } else { + host = url; + } + } else { + builder.subtype("directory"); + final String urlInstance = details; + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } + + if (host != null) { + builder.host(host); + } + return builder.instance(instance); + } + }; + + private static final Map typeParsers = new HashMap<>(); + + static { + for (final JDBCConnectionUrlParser parser : JDBCConnectionUrlParser.values()) { + for (final String key : parser.typeKeys) { + typeParsers.put(key, parser); + } + } + } + + private final String[] typeKeys; + + JDBCConnectionUrlParser(final String... typeKeys) { + this.typeKeys = typeKeys; + } + + abstract DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder); + + public static DBInfo parse(String connectionUrl, final Properties props) { + if (connectionUrl == null) { + return DEFAULT; + } + // Make this easier and ignore case. + connectionUrl = connectionUrl.toLowerCase(); + + if (!connectionUrl.startsWith("jdbc:")) { + return DEFAULT; + } + + final String jdbcUrl = connectionUrl.substring("jdbc:".length()); + final int typeLoc = jdbcUrl.indexOf(':'); + + if (typeLoc < 1) { + // Invalid format: `jdbc:` or `jdbc::` + return DEFAULT; + } + + final String baseType = jdbcUrl.substring(0, typeLoc); + + final DBInfo.Builder parsedProps = DEFAULT.toBuilder().type(baseType); + populateStandardProperties(parsedProps, props); + + try { + if (typeParsers.containsKey(baseType)) { + // Delegate to specific parser + return typeParsers.get(baseType).doParse(jdbcUrl, parsedProps).build(); + } + return GENERIC_URL_LIKE.doParse(connectionUrl, parsedProps).build(); + } catch (final Exception e) { + ExceptionLogger.LOGGER.debug("Error parsing URL", e); + return parsedProps.build(); + } + } + + // Source: https://stackoverflow.com/a/13592567 + private static Map splitQuery(final String query, final String separator) { + if (query == null || query.isEmpty()) { + return Collections.emptyMap(); + } + final Map query_pairs = new LinkedHashMap<>(); + final String[] pairs = query.split(separator); + for (final String pair : pairs) { + try { + final int idx = pair.indexOf("="); + final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; + if (!query_pairs.containsKey(key)) { + final String value = + idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") + : null; + query_pairs.put(key, value); + } + } catch (final UnsupportedEncodingException e) { + // Ignore. + } + } + return query_pairs; + } + + private static void populateStandardProperties( + final DBInfo.Builder builder, final Map props) { + if (props != null && !props.isEmpty()) { + if (props.containsKey("user")) { + builder.user((String) props.get("user")); + } + + if (props.containsKey("databasename")) { + builder.db((String) props.get("databasename")); + } + if (props.containsKey("databaseName")) { + builder.db((String) props.get("databaseName")); + } + + if (props.containsKey("servername")) { + builder.host((String) props.get("servername")); + } + if (props.containsKey("serverName")) { + builder.host((String) props.get("serverName")); + } + + if (props.containsKey("portnumber")) { + final String portNumber = (String) props.get("portnumber"); + try { + builder.port(Integer.parseInt(portNumber)); + } catch (final NumberFormatException e) { + ExceptionLogger.LOGGER.debug("Error parsing portnumber property: " + portNumber, e); + } + } + + if (props.containsKey("portNumber")) { + final String portNumber = (String) props.get("portNumber"); + try { + builder.port(Integer.parseInt(portNumber)); + } catch (final NumberFormatException e) { + ExceptionLogger.LOGGER.debug("Error parsing portNumber property: " + portNumber, 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 7de8cca72f..8c5f12a7b0 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 @@ -10,9 +10,11 @@ import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; -public class JDBCDecorator extends DatabaseClientDecorator { +public class JDBCDecorator extends DatabaseClientDecorator { public static final JDBCDecorator DECORATE = new JDBCDecorator(); + private static final String DB_QUERY = "DB Query"; + @Override protected String[] instrumentationNames() { return new String[] {"jdbc"}; @@ -39,22 +41,27 @@ public class JDBCDecorator extends DatabaseClientDecorator { } @Override - protected String dbUser(final JDBCMaps.DBInfo info) { + protected String dbUser(final DBInfo info) { return info.getUser(); } @Override - protected String dbInstance(final JDBCMaps.DBInfo info) { - return info.getUrl(); + protected String dbInstance(final DBInfo info) { + if (info.getInstance() != null) { + return info.getInstance(); + } else { + return info.getDb(); + } } public Span onConnection(final Span span, final Connection connection) { - JDBCMaps.DBInfo dbInfo = JDBCMaps.connectionInfo.get(connection); + DBInfo dbInfo = JDBCMaps.connectionInfo.get(connection); /** - * Logic to get the DBInfo from a JDBC Connection, if the connection was never seen before, the - * connectionInfo map will return null and will attempt to extract DBInfo from the connection. - * If the DBInfo can't be extracted, then the connection will be stored with the DEFAULT DBInfo - * as the value in the connectionInfo map to avoid retry overhead. + * Logic to get the DBInfo from a JDBC Connection, if the connection was not created via + * Driver.connect, or it has never seen before, the connectionInfo map will return null and will + * attempt to extract DBInfo from the connection. If the DBInfo can't be extracted, then the + * connection will be stored with the DEFAULT DBInfo as the value in the connectionInfo map to + * avoid retry overhead. */ { if (dbInfo == null) { @@ -62,19 +69,12 @@ public class JDBCDecorator extends DatabaseClientDecorator { final DatabaseMetaData metaData = connection.getMetaData(); final String url = metaData.getURL(); if (url != null) { - // Remove end of url to prevent passwords from leaking: - final String sanitizedURL = url.replaceAll("[?;].*", ""); - final String type = url.split(":", -1)[1]; - String user = metaData.getUserName(); - if (user != null && user.trim().equals("")) { - user = null; - } - dbInfo = new JDBCMaps.DBInfo(sanitizedURL, type, user); + dbInfo = JDBCConnectionUrlParser.parse(url, connection.getClientInfo()); } else { - dbInfo = JDBCMaps.DBInfo.DEFAULT; + dbInfo = DBInfo.DEFAULT; } } catch (final SQLException se) { - dbInfo = JDBCMaps.DBInfo.DEFAULT; + dbInfo = DBInfo.DEFAULT; } JDBCMaps.connectionInfo.put(connection, dbInfo); } @@ -89,7 +89,7 @@ public class JDBCDecorator extends DatabaseClientDecorator { @Override public Span onStatement(final Span span, final String statement) { - final String resourceName = statement == null ? JDBCMaps.DB_QUERY : statement; + final String resourceName = statement == null ? DB_QUERY : statement; span.setTag(DDTags.RESOURCE_NAME, resourceName); Tags.COMPONENT.set(span, "java-jdbc-statement"); return super.onStatement(span, statement); @@ -97,7 +97,7 @@ public class JDBCDecorator extends DatabaseClientDecorator { public Span onPreparedStatement(final Span span, final PreparedStatement statement) { final String sql = JDBCMaps.preparedStatements.get(statement); - final String resourceName = sql == null ? JDBCMaps.DB_QUERY : sql; + final String resourceName = sql == null ? DB_QUERY : sql; span.setTag(DDTags.RESOURCE_NAME, resourceName); Tags.COMPONENT.set(span, "java-jdbc-prepared_statement"); return super.onStatement(span, sql); diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCMaps.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCMaps.java index 13bbe8865e..1d91031775 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCMaps.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCMaps.java @@ -5,7 +5,6 @@ import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap; import datadog.trace.bootstrap.WeakMap; import java.sql.Connection; import java.sql.PreparedStatement; -import lombok.Data; /** * JDBC instrumentation shares a global map of connection info. @@ -15,14 +14,4 @@ import lombok.Data; public class JDBCMaps { public static final WeakMap connectionInfo = newWeakMap(); public static final WeakMap preparedStatements = newWeakMap(); - - public static final String DB_QUERY = "DB Query"; - - @Data - public static class DBInfo { - public static DBInfo DEFAULT = new DBInfo("null", "database", null); - private final String url; - private final String type; - private final String user; - } } diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCUtils.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCUtils.java index 0fb70291f6..a7883679cc 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCUtils.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCUtils.java @@ -1,10 +1,12 @@ package datadog.trace.instrumentation.jdbc; import datadog.trace.bootstrap.ExceptionLogger; +import java.lang.reflect.Field; import java.sql.Connection; import java.sql.Statement; public abstract class JDBCUtils { + private static Field c3poField = null; /** * @param statement @@ -14,6 +16,13 @@ public abstract class JDBCUtils { Connection connection; try { connection = statement.getConnection(); + + if (c3poField != null) { + if (connection.getClass().getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { + return (Connection) c3poField.get(connection); + } + } + try { // unwrap the connection to cache the underlying actual connection and to not cache proxy // objects @@ -21,6 +30,15 @@ public abstract class JDBCUtils { connection = connection.unwrap(Connection.class); } } catch (final Exception | AbstractMethodError e) { + // Attempt to work around c3po delegating to an connection that doesn't support unwrapping. + final Class connectionClass = connection.getClass(); + if (connectionClass.getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { + final Field inner = connectionClass.getDeclaredField("inner"); + inner.setAccessible(true); + c3poField = inner; + return (Connection) c3poField.get(connection); + } + // perhaps wrapping isn't supported? // ex: org.h2.jdbc.JdbcConnection v1.3.175 // or: jdts.jdbc which always throws `AbstractMethodError` (at least up to version 1.3) diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java index 56e73233d1..2242387c1d 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java @@ -20,6 +20,8 @@ import io.opentracing.noop.NoopScopeManager; import io.opentracing.util.GlobalTracer; import java.sql.Connection; import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; @@ -40,15 +42,23 @@ public final class PreparedStatementInstrumentation extends Instrumenter.Default @Override public String[] helperClassNames() { - return new String[] { - "datadog.trace.agent.decorator.BaseDecorator", - "datadog.trace.agent.decorator.ClientDecorator", - "datadog.trace.agent.decorator.DatabaseClientDecorator", - packageName + ".JDBCDecorator", - packageName + ".JDBCMaps", - packageName + ".JDBCMaps$DBInfo", - packageName + ".JDBCUtils", - }; + final List helpers = new ArrayList<>(JDBCConnectionUrlParser.values().length + 9); + + helpers.add(packageName + ".DBInfo"); + helpers.add(packageName + ".DBInfo$Builder"); + helpers.add(packageName + ".JDBCUtils"); + helpers.add(packageName + ".JDBCMaps"); + helpers.add(packageName + ".JDBCConnectionUrlParser"); + + helpers.add("datadog.trace.agent.decorator.BaseDecorator"); + helpers.add("datadog.trace.agent.decorator.ClientDecorator"); + helpers.add("datadog.trace.agent.decorator.DatabaseClientDecorator"); + helpers.add(packageName + ".JDBCDecorator"); + + for (final JDBCConnectionUrlParser parser : JDBCConnectionUrlParser.values()) { + helpers.add(parser.getClass().getName()); + } + return helpers.toArray(new String[0]); } @Override 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 a39e574f8d..5c0fb81177 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 @@ -20,6 +20,8 @@ import io.opentracing.noop.NoopScopeManager; import io.opentracing.util.GlobalTracer; import java.sql.Connection; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; @@ -40,15 +42,23 @@ public final class StatementInstrumentation extends Instrumenter.Default { @Override public String[] helperClassNames() { - return new String[] { - "datadog.trace.agent.decorator.BaseDecorator", - "datadog.trace.agent.decorator.ClientDecorator", - "datadog.trace.agent.decorator.DatabaseClientDecorator", - packageName + ".JDBCDecorator", - packageName + ".JDBCMaps", - packageName + ".JDBCMaps$DBInfo", - packageName + ".JDBCUtils", - }; + final List helpers = new ArrayList<>(JDBCConnectionUrlParser.values().length + 9); + + helpers.add(packageName + ".DBInfo"); + helpers.add(packageName + ".DBInfo$Builder"); + helpers.add(packageName + ".JDBCUtils"); + helpers.add(packageName + ".JDBCMaps"); + helpers.add(packageName + ".JDBCConnectionUrlParser"); + + helpers.add("datadog.trace.agent.decorator.BaseDecorator"); + helpers.add("datadog.trace.agent.decorator.ClientDecorator"); + helpers.add("datadog.trace.agent.decorator.DatabaseClientDecorator"); + helpers.add(packageName + ".JDBCDecorator"); + + for (final JDBCConnectionUrlParser parser : JDBCConnectionUrlParser.values()) { + helpers.add(parser.getClass().getName()); + } + return helpers.toArray(new String[0]); } @Override diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCConnectionUrlParserTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCConnectionUrlParserTest.groovy new file mode 100644 index 0000000000..e0fb31854a --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCConnectionUrlParserTest.groovy @@ -0,0 +1,181 @@ +import datadog.trace.instrumentation.jdbc.DBInfo +import spock.lang.Shared +import spock.lang.Specification + +import static datadog.trace.instrumentation.jdbc.JDBCConnectionUrlParser.parse + +class JDBCConnectionUrlParserTest extends Specification { + + @Shared + def stdProps = { + def prop = new Properties() + // https://download.oracle.com/otn-pub/jcp/jdbc-4_1-mrel-spec/jdbc4.1-fr-spec.pdf + prop.setProperty("databaseName", "stdDatabaseName") + prop.setProperty("dataSourceName", "stdDatasourceName") + prop.setProperty("description", "Some description") + prop.setProperty("networkProtocol", "stdProto") + prop.setProperty("password", "PASSWORD!") + prop.setProperty("portNumber", "9999") + prop.setProperty("roleName", "stdRoleName") + prop.setProperty("serverName", "stdServerName") + prop.setProperty("user", "stdUserName") + return prop + }() + + def "invalid url returns default"() { + expect: + parse(url, null) == DBInfo.DEFAULT + + where: + url | _ + null | _ + "" | _ + "jdbc:" | _ + "jdbc::" | _ + "bogus:string" | _ + } + + def "verify #type:#subtype parsing of #url"() { + setup: + def info = parse(url, props) + + expect: + info.url == expected.url + info.type == expected.type + info.host == expected.host + info.port == expected.port + info.user == expected.user + info.instance == expected.instance + + info == expected + + where: + url | props | type | subtype | user | host | port | instance | db + // https://jdbc.postgresql.org/documentation/94/connect.html + "jdbc:postgresql:///" | null | "postgresql" | null | null | "localhost" | 5432 | null | null + "jdbc:postgresql:///" | stdProps | "postgresql" | null | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:postgresql://pg.host" | null | "postgresql" | null | null | "pg.host" | 5432 | null | null + "jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW" | null | "postgresql" | null | "pguser" | "pg.host" | 11 | null | "pgdb" + "jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW" | stdProps | "postgresql" | null | "pguser" | "pg.host" | 11 | null | "pgdb" + + // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html + // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html + "jdbc:mysql:///" | null | "mysql" | null | null | "localhost" | 3306 | null | null + "jdbc:mysql:///" | stdProps | "mysql" | null | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:mysql://my.host" | null | "mysql" | null | null | "my.host" | 3306 | null | null + "jdbc:mysql://my.host?user=myuser&password=PW" | null | "mysql" | null | "myuser" | "my.host" | 3306 | null | null + "jdbc:mysql://my.host:22/mydb?user=myuser&password=PW" | null | "mysql" | null | "myuser" | "my.host" | 22 | null | "mydb" + "jdbc:mysql://127.0.0.1:22/mydb?user=myuser&password=PW" | stdProps | "mysql" | null | "myuser" | "127.0.0.1" | 22 | null | "mydb" + + // https://mariadb.com/kb/en/library/about-mariadb-connector-j/#connection-strings + "jdbc:mariadb:127.0.0.1:33/mdbdb" | null | "mariadb" | null | null | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mariadb:localhost/mdbdb" | null | "mariadb" | null | null | "localhost" | 3306 | null | "mdbdb" + "jdbc:mariadb:localhost/mdbdb?user=mdbuser&password=PW" | stdProps | "mariadb" | null | "mdbuser" | "localhost" | 9999 | null | "mdbdb" + "jdbc:mariadb:localhost:33/mdbdb" | stdProps | "mariadb" | null | "stdUserName" | "localhost" | 33 | null | "mdbdb" + "jdbc:mariadb://mdb.host:33/mdbdb?user=mdbuser&password=PW" | null | "mariadb" | null | "mdbuser" | "mdb.host" | 33 | null | "mdbdb" + "jdbc:mariadb:aurora://mdb.host/mdbdb" | null | "mariadb" | "aurora" | null | "mdb.host" | 3306 | null | "mdbdb" + "jdbc:mysql:aurora://mdb.host/mdbdb" | null | "mysql" | "aurora" | null | "mdb.host" | 3306 | null | "mdbdb" + "jdbc:mysql:failover://localhost/mdbdb?autoReconnect=true" | null | "mysql" | "failover" | null | "localhost" | 3306 | null | "mdbdb" + "jdbc:mariadb:failover://mdb.host1:33,mdb.host/mdbdb?characterEncoding=utf8" | null | "mariadb" | "failover" | null | "mdb.host1" | 33 | null | "mdbdb" + "jdbc:mariadb:sequential://mdb.host1,mdb.host2:33/mdbdb" | null | "mariadb" | "sequential" | null | "mdb.host1" | 3306 | null | "mdbdb" + "jdbc:mariadb:loadbalance://127.0.0.1:33,mdb.host/mdbdb" | null | "mariadb" | "loadbalance" | null | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mariadb:loadbalance://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:33,mdb.host/mdbdb" | null | "mariadb" | "loadbalance" | null | "2001:0660:7401:0200:0000:0000:0edf:bdd7" | 33 | null | "mdbdb" + "jdbc:mysql:loadbalance://127.0.0.1,127.0.0.1:3306/mdbdb?user=mdbuser&password=PW" | null | "mysql" | "loadbalance" | "mdbuser" | "127.0.0.1" | 3306 | null | "mdbdb" + "jdbc:mariadb:replication://localhost:33,anotherhost:3306/mdbdb" | null | "mariadb" | "replication" | null | "localhost" | 33 | null | "mdbdb" + "jdbc:mysql:replication://address=(HOST=127.0.0.1)(port=33)(user=mdbuser)(password=PW)," + + "address=(host=mdb.host)(port=3306)(user=otheruser)(password=PW)/mdbdb?user=wrong&password=PW" | null | "mysql" | "replication" | "mdbuser" | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mysql:replication://address=(HOST=mdb.host)," + + "address=(host=anotherhost)(port=3306)(user=wrong)(password=PW)/mdbdb?user=mdbuser&password=PW" | null | "mysql" | "replication" | "mdbuser" | "mdb.host" | 3306 | null | "mdbdb" + + //https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + "jdbc:microsoft:sqlserver://;" | null | "sqlserver" | null | null | "localhost" | 1433 | "MSSQLSERVER" | null + "jdbc:microsoft:sqlserver://;" | stdProps | "sqlserver" | null | "stdUserName" | "stdServerName" | 9999 | "MSSQLSERVER" | "stdDatabaseName" + "jdbc:sqlserver://ss.host\\ssinstance:44;databaseName=ssdb;user=ssuser;password=pw" | null | "sqlserver" | null | "ssuser" | "ss.host" | 44 | "ssinstance" | "ssdb" + "jdbc:sqlserver://;serverName=ss.host\\ssinstance:44;DatabaseName=;" | null | "sqlserver" | null | null | "ss.host" | 44 | "ssinstance" | null + "jdbc:sqlserver://ss.host;serverName=althost;DatabaseName=ssdb;" | null | "sqlserver" | null | null | "ss.host" | 1433 | "MSSQLSERVER" | "ssdb" + "jdbc:microsoft:sqlserver://ss.host:44;DatabaseName=ssdb;user=ssuser;password=pw;user=ssuser2;" | null | "sqlserver" | null | "ssuser" | "ss.host" | 44 | "MSSQLSERVER" | "ssdb" + + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/urls.htm + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/jdbcthin.htm + "jdbc:oracle:thin:orcluser/PW@localhost:55:orclsn" | null | "oracle" | "thin" | "orcluser" | "localhost" | 55 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@//orcl.host:55/orclsn" | null | "oracle" | "thin" | "orcluser" | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@127.0.0.1:orclsn" | null | "oracle" | "thin" | "orcluser" | "127.0.0.1" | 1521 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@//orcl.host/orclsn" | null | "oracle" | "thin" | "orcluser" | "orcl.host" | 1521 | "orclsn" | null + "jdbc:oracle:thin:@//orcl.host:55/orclsn" | null | "oracle" | "thin" | null | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:thin:@ldap://orcl.host:55/some,cn=OracleContext,dc=com" | null | "oracle" | "thin" | null | "orcl.host" | 55 | "some,cn=oraclecontext,dc=com" | null + "jdbc:oracle:thin:127.0.0.1:orclsn" | null | "oracle" | "thin" | null | "127.0.0.1" | 1521 | "orclsn" | null + "jdbc:oracle:thin:orcl.host:orclsn" | stdProps | "oracle" | "thin" | "stdUserName" | "orcl.host" | 9999 | "orclsn" | "stdDatabaseName" + "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST= 127.0.0.1 )(POR T= 666))" + + "(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orclsn)))" | null | "oracle" | "thin" | null | "127.0.0.1" | 1521 | "orclsn" | null + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/instclnt.htm + "jdbc:oracle:drivertype:orcluser/PW@orcl.host:55/orclsn" | null | "oracle" | "drivertype" | "orcluser" | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:oci8:@" | null | "oracle" | "oci8" | null | null | 1521 | null | null + "jdbc:oracle:oci8:@" | stdProps | "oracle" | "oci8" | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:oracle:oci8:@orclsn" | null | "oracle" | "oci8" | null | null | 1521 | "orclsn" | null + "jdbc:oracle:oci:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)( HOST = orcl.host )" + + "( PORT = 55 ))(CONNECT_DATA=(SERVICE_NAME =orclsn )))" | null | "oracle" | "oci" | null | "orcl.host" | 55 | "orclsn" | null + + // https://www.ibm.com/support/knowledgecenter/en/SSEPEK_10.0.0/java/src/tpc/imjcc_tjvjcccn.html + // https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.5.0/com.ibm.db2.luw.apdv.java.doc/src/tpc/imjcc_r0052342.html + "jdbc:db2://db2.host" | null | "db2" | null | null | "db2.host" | 50000 | null | null + "jdbc:db2://db2.host" | stdProps | "db2" | null | "stdUserName" | "db2.host" | 9999 | null | "stdDatabaseName" + "jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;" | null | "db2" | null | "db2user" | "db2.host" | 77 | "db2db" | null + "jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;" | stdProps | "db2" | null | "db2user" | "db2.host" | 77 | "db2db" | "stdDatabaseName" + "jdbc:as400://ashost:66/asdb:user=asuser;password=PW;" | null | "as400" | null | "asuser" | "ashost" | 66 | "asdb" | null + + // https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/2.0.03/en-US/ff15928cf5594d78b841fbbe649f04b4.html + "jdbc:sap://sap.host" | null | "sap" | null | null | "sap.host" | null | null | null + "jdbc:sap://sap.host" | stdProps | "sap" | null | "stdUserName" | "sap.host" | 9999 | null | "stdDatabaseName" + "jdbc:sap://sap.host:88/?databaseName=sapdb&user=sapuser&password=PW" | null | "sap" | null | "sapuser" | "sap.host" | 88 | null | "sapdb" + + // TODO: +// "jdbc:informix-sqli://infxhost:99/infxdb:INFORMIXSERVER=infxsn;user=infxuser;password=PW" | null | "informix-sqli" | null | "infxuser" | "infxhost" | 99 | "infxdb"| null +// "jdbc:informix-direct://infxdb:999;user=infxuser;password=PW" | null | "informix-direct" | null | "infxuser" | "infxhost" | 999 | "infxdb"| null + + // http://www.h2database.com/html/features.html#database_url + "jdbc:h2:mem:" | null | "h2" | "mem" | null | null | null | null | null + "jdbc:h2:mem:" | stdProps | "h2" | "mem" | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:h2:mem:h2db" | null | "h2" | "mem" | null | null | null | "h2db" | null + "jdbc:h2:tcp://h2.host:111/path/h2db;user=h2user;password=PW" | null | "h2" | "tcp" | "h2user" | "h2.host" | 111 | "path/h2db" | null + "jdbc:h2:ssl://h2.host:111/path/h2db;user=h2user;password=PW" | null | "h2" | "ssl" | "h2user" | "h2.host" | 111 | "path/h2db" | null + "jdbc:h2:/data/h2file" | null | "h2" | "file" | null | null | null | "/data/h2file" | null + "jdbc:h2:file:~/h2file;USER=h2user;PASSWORD=PW" | null | "h2" | "file" | null | null | null | "~/h2file" | null + "jdbc:h2:file:/data/h2file" | null | "h2" | "file" | null | null | null | "/data/h2file" | null + "jdbc:h2:file:C:/data/h2file" | null | "h2" | "file" | null | null | null | "c:/data/h2file" | null + "jdbc:h2:zip:~/db.zip!/h2zip" | null | "h2" | "zip" | null | null | null | "~/db.zip!/h2zip" | null + + // http://hsqldb.org/doc/2.0/guide/dbproperties-chapt.html + "jdbc:hsqldb:hsdb" | null | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:hsdb" | stdProps | "hsqldb" | "mem" | "stdUserName" | "stdServerName" | 9999 | "hsdb" | "stdDatabaseName" + "jdbc:hsqldb:mem:hsdb" | null | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:hsdb" | null | "hsqldb" | "file" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:/loc/hsdb" | null | "hsqldb" | "file" | "SA" | null | null | "/loc/hsdb" | null + "jdbc:hsqldb:file:C:/hsdb" | null | "hsqldb" | "file" | "SA" | null | null | "c:/hsdb" | null + "jdbc:hsqldb:res:hsdb" | null | "hsqldb" | "res" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:res:/cp/hsdb" | null | "hsqldb" | "res" | "SA" | null | null | "/cp/hsdb" | null + "jdbc:hsqldb:hsql://hs.host:333/hsdb" | null | "hsqldb" | "hsql" | "SA" | "hs.host" | 333 | "hsdb" | null + "jdbc:hsqldb:hsqls://hs.host/hsdb" | null | "hsqldb" | "hsqls" | "SA" | "hs.host" | 9001 | "hsdb" | null + "jdbc:hsqldb:http://hs.host" | null | "hsqldb" | "http" | "SA" | "hs.host" | 80 | null | null + "jdbc:hsqldb:http://hs.host:333/hsdb" | null | "hsqldb" | "http" | "SA" | "hs.host" | 333 | "hsdb" | null + "jdbc:hsqldb:https://127.0.0.1/hsdb" | null | "hsqldb" | "https" | "SA" | "127.0.0.1" | 443 | "hsdb" | null + + // https://db.apache.org/derby/papers/DerbyClientSpec.html#Connection+URL+Format + // https://db.apache.org/derby/docs/10.8/devguide/cdevdvlp34964.html + "jdbc:derby:derbydb" | null | "derby" | "directory" | "APP" | null | null | "derbydb" | null + "jdbc:derby:derbydb" | stdProps | "derby" | "directory" | "stdUserName" | "stdServerName" | 9999 | "derbydb" | "stdDatabaseName" + "jdbc:derby:derbydb;user=derbyuser;password=pw" | null | "derby" | "directory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby:memory:derbydb" | null | "derby" | "memory" | "APP" | null | null | "derbydb" | null + "jdbc:derby:memory:;databaseName=derbydb" | null | "derby" | "memory" | "APP" | null | null | null | "derbydb" + "jdbc:derby:memory:derbydb;databaseName=altdb" | null | "derby" | "memory" | "APP" | null | null | "derbydb" | "altdb" + "jdbc:derby:memory:derbydb;user=derbyuser;password=pw" | null | "derby" | "memory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby://derby.host:222/memory:derbydb;create=true" | null | "derby" | "network" | "APP" | "derby.host" | 222 | "derbydb" | null + "jdbc:derby://derby.host/memory:derbydb;create=true;user=derbyuser;password=pw" | null | "derby" | "network" | "derbyuser" | "derby.host" | 1527 | "derbydb" | null + "jdbc:derby://127.0.0.1:1527/memory:derbydb;create=true;user=derbyuser;password=pw" | null | "derby" | "network" | "derbyuser" | "127.0.0.1" | 1527 | "derbydb" | null + "jdbc:derby:directory:derbydb;user=derbyuser;password=pw" | null | "derby" | "directory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby:classpath:/some/derbydb;user=derbyuser;password=pw" | null | "derby" | "classpath" | "derbyuser" | null | null | "/some/derbydb" | null + "jdbc:derby:jar:/derbydb;user=derbyuser;password=pw" | null | "derby" | "jar" | "derbyuser" | null | null | "/derbydb" | null + "jdbc:derby:jar:(~/path/to/db.jar)/other/derbydb;user=derbyuser;password=pw" | null | "derby" | "jar" | "derbyuser" | null | null | "(~/path/to/db.jar)/other/derbydb" | null + + expected = new DBInfo.Builder().type(type).subtype(subtype).user(user).instance(instance).db(db).host(host).port(port).build() + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy index cc5a0e1ee5..9ecc358c19 100644 --- a/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy @@ -4,13 +4,13 @@ import com.zaxxer.hikari.HikariDataSource import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.DDSpanTypes import io.opentracing.tag.Tags +import javax.sql.DataSource import org.apache.derby.jdbc.EmbeddedDriver import org.h2.Driver 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 @@ -26,32 +26,42 @@ class JDBCInstrumentationTest extends AgentTestRunner { @Shared private Map jdbcUrls = [ - h2 : "jdbc:h2:mem:" + dbName, - derby : "jdbc:derby:memory:" + dbName, - hsqldb: "jdbc:hsqldb:mem:" + dbName + "h2" : "jdbc:h2:mem:$dbName", + "derby" : "jdbc:derby:memory:$dbName", + "hsqldb": "jdbc:hsqldb:mem:$dbName", ] @Shared private Map jdbcDriverClassNames = [ - h2 : "org.h2.Driver", - derby : "org.apache.derby.jdbc.EmbeddedDriver", - hsqldb: "org.hsqldb.jdbc.JDBCDriver" + "h2" : "org.h2.Driver", + "derby" : "org.apache.derby.jdbc.EmbeddedDriver", + "hsqldb": "org.hsqldb.jdbc.JDBCDriver", ] @Shared private Map jdbcUserNames = [ - h2 : null, - derby : "APP", - hsqldb: "SA" + "h2" : null, + "derby" : "APP", + "hsqldb": "SA", ] + @Shared + private Properties connectionProps = { + def props = new Properties() +// props.put("user", "someUser") +// props.put("password", "somePassword") + props.put("databaseName", "someDb") + props.put("OPEN_NEW", "true") // So H2 doesn't complain about username/password. + return props + }() + // JDBC Connection pool name (i.e. HikariCP) -> Map @Shared private Map> cpDatasources = new HashMap<>() def prepareConnectionPoolDatasources() { String[] connectionPoolNames = [ - "tomcat", "hikari", "c3p0" + "tomcat", "hikari", "c3p0", ] connectionPoolNames.each { cpName -> @@ -170,7 +180,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -183,19 +193,22 @@ class JDBCInstrumentationTest extends AgentTestRunner { 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" - "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null) | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" - "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" - "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" - "hsqldb" | cpDatasources.get("tomcat").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" - "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" - "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" - "hsqldb" | cpDatasources.get("hikari").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" - "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" - "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" - "hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + 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" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null) | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | new Driver().connect(jdbcUrls.get("h2"), connectionProps) | null | "SELECT 3" + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), connectionProps) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), connectionProps) | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("tomcat").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("hikari").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" } @Unroll @@ -230,7 +243,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-prepared_statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -285,7 +298,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-prepared_statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -340,7 +353,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-prepared_statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -395,7 +408,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -453,7 +466,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { } "span.kind" Tags.SPAN_KIND_CLIENT "component" "java-jdbc-prepared_statement" - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -528,7 +541,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { "component" "java-jdbc-statement" } "span.kind" Tags.SPAN_KIND_CLIENT - "db.instance" jdbcUrls.get(driver) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -585,7 +598,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { res[i] == 3 } assertTraces(5) { - trace(0, 2) { + trace(0, 1) { span(0) { operationName "${dbType}.query" serviceName dbType @@ -597,24 +610,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { "db.user" "SA" "component" "java-jdbc-prepared_statement" "span.kind" Tags.SPAN_KIND_CLIENT - "db.instance" jdbcUrls.get(dbType) - "span.origin.type" String - defaultTags() - } - } - span(1) { - operationName "${dbType}.query" - serviceName dbType - resourceName "CALL USER()" - spanType DDSpanTypes.SQL - errored false - childOf(span(0)) - tags { - "db.type" "hsqldb" - "db.user" "SA" - "component" "java-jdbc-statement" - "span.kind" Tags.SPAN_KIND_CLIENT - "db.instance" jdbcUrls.get(dbType) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } @@ -633,7 +629,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { "db.user" "SA" "component" "java-jdbc-prepared_statement" "span.kind" Tags.SPAN_KIND_CLIENT - "db.instance" jdbcUrls.get(dbType) + "db.instance" dbName.toLowerCase() "span.origin.type" String defaultTags() } diff --git a/dd-java-agent/instrumentation/lettuce-5/src/main/java8/datadog/trace/instrumentation/lettuce/LettuceClientDecorator.java b/dd-java-agent/instrumentation/lettuce-5/src/main/java8/datadog/trace/instrumentation/lettuce/LettuceClientDecorator.java index 491bdb6448..f05674b500 100644 --- a/dd-java-agent/instrumentation/lettuce-5/src/main/java8/datadog/trace/instrumentation/lettuce/LettuceClientDecorator.java +++ b/dd-java-agent/instrumentation/lettuce-5/src/main/java8/datadog/trace/instrumentation/lettuce/LettuceClientDecorator.java @@ -43,7 +43,7 @@ public class LettuceClientDecorator extends DatabaseClientDecorator { @Override protected String dbInstance(final RedisURI connection) { - return connection.getHost() + ":" + connection.getPort() + "/" + connection.getDatabase(); + return null; } @Override @@ -53,7 +53,8 @@ public class LettuceClientDecorator extends DatabaseClientDecorator { Tags.PEER_PORT.set(span, connection.getPort()); span.setTag("db.redis.dbIndex", connection.getDatabase()); - span.setTag(DDTags.RESOURCE_NAME, "CONNECT:" + dbInstance(connection)); + span.setTag(DDTags.RESOURCE_NAME, "CONNECT:" + connection.getHost() + + ":" + connection.getPort() + "/" + connection.getDatabase()); } return super.onConnection(span, connection); } diff --git a/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceAsyncClientTest.groovy b/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceAsyncClientTest.groovy index 81ab5a9da5..5af20d06d0 100644 --- a/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceAsyncClientTest.groovy +++ b/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceAsyncClientTest.groovy @@ -123,7 +123,6 @@ class LettuceAsyncClientTest extends AgentTestRunner { tags { defaultTags() "component" "redis-client" - "db.instance" dbAddr "db.redis.dbIndex" 0 "db.type" "redis" "peer.hostname" HOST @@ -163,7 +162,6 @@ class LettuceAsyncClientTest extends AgentTestRunner { tags { defaultTags() "component" "redis-client" - "db.instance" dbAddrNonExistent "db.redis.dbIndex" 0 "db.type" "redis" errorTags CompletionException, String diff --git a/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceSyncClientTest.groovy b/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceSyncClientTest.groovy index 9bb25e2416..f328eb240b 100644 --- a/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceSyncClientTest.groovy +++ b/dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceSyncClientTest.groovy @@ -103,7 +103,6 @@ class LettuceSyncClientTest extends AgentTestRunner { tags { defaultTags() "component" "redis-client" - "db.instance" dbAddr "db.redis.dbIndex" 0 "db.type" "redis" "peer.hostname" HOST @@ -140,7 +139,6 @@ class LettuceSyncClientTest extends AgentTestRunner { tags { defaultTags() "component" "redis-client" - "db.instance" dbAddrNonExistent "db.redis.dbIndex" 0 "db.type" "redis" errorTags CompletionException, String diff --git a/dd-java-agent/instrumentation/mongo-3.1/src/test/groovy/MongoClientDecoratorTest.groovy b/dd-java-agent/instrumentation/mongo-3.1/src/test/groovy/MongoClientDecoratorTest.groovy deleted file mode 100644 index 4ddc61f661..0000000000 --- a/dd-java-agent/instrumentation/mongo-3.1/src/test/groovy/MongoClientDecoratorTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -import datadog.trace.api.DDTags -import io.opentracing.Span -import io.opentracing.tag.Tags -import org.bson.BsonArray -import org.bson.BsonDocument -import org.bson.BsonString -import spock.lang.Shared -import spock.lang.Specification - -import static datadog.trace.instrumentation.mongo.MongoClientDecorator.DECORATE - -class MongoClientDecoratorTest extends Specification { - - @Shared - def query1, query2 - - def setupSpec() { - query1 = new BsonDocument("find", new BsonString("show")) - query1.put("stuff", new BsonString("secret")) - - - query2 = new BsonDocument("insert", new BsonString("table")) - def nestedDoc = new BsonDocument("count", new BsonString("show")) - nestedDoc.put("id", new BsonString("secret")) - query2.put("docs", new BsonArray(Arrays.asList(new BsonString("secret"), nestedDoc))) - } - - def "test query scrubbing"() { - setup: - def span = Mock(Span) - // all "secret" strings should be scrubbed out of these queries - - when: - DECORATE.onStatement(span, query) - - then: - 1 * span.setTag(Tags.DB_STATEMENT.key, expected) - 1 * span.setTag(DDTags.RESOURCE_NAME, expected) - 0 * _ - - where: - query << [query1, query2] - expected = query.toString().replaceAll("secret", "?") - } -} diff --git a/dd-java-agent/instrumentation/mongo-3.1/src/test/java/datadog/trace/instrumentation/mongo/MongoClientInstrumentationTest.java b/dd-java-agent/instrumentation/mongo-3.1/src/test/java/datadog/trace/instrumentation/mongo/MongoClientInstrumentationTest.java deleted file mode 100644 index e3dd177876..0000000000 --- a/dd-java-agent/instrumentation/mongo-3.1/src/test/java/datadog/trace/instrumentation/mongo/MongoClientInstrumentationTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package datadog.trace.instrumentation.mongo; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -import com.mongodb.ServerAddress; -import com.mongodb.connection.ClusterId; -import com.mongodb.connection.ConnectionDescription; -import com.mongodb.connection.ServerId; -import com.mongodb.event.CommandStartedEvent; -import datadog.opentracing.DDSpan; -import datadog.opentracing.DDTracer; -import datadog.trace.api.DDSpanTypes; -import io.opentracing.tag.Tags; -import java.util.Arrays; -import java.util.List; -import org.bson.BsonArray; -import org.bson.BsonDocument; -import org.bson.BsonString; -import org.junit.Test; - -public class MongoClientInstrumentationTest { - - private static ConnectionDescription makeConnection() { - return new ConnectionDescription(new ServerId(new ClusterId(), new ServerAddress())); - } - - @Test - public void mongoSpan() { - final CommandStartedEvent cmd = - new CommandStartedEvent(1, makeConnection(), "databasename", "query", new BsonDocument()); - - final DDSpan span = new DDTracer().buildSpan("foo").start(); - MongoClientDecorator.DECORATE.afterStart(span); - MongoClientDecorator.DECORATE.onStatement(span, cmd.getCommand()); - - assertThat(span.context().getSpanType()).isEqualTo("mongodb"); - assertThat(span.context().getResourceName()) - .isEqualTo(span.context().getTags().get("db.statement")); - assertThat(span.getSpanType()).isEqualTo(DDSpanTypes.MONGO); - } - - @Test - public void queryScrubbing() { - // all "secret" strings should be scrubbed out of these queries - final BsonDocument query1 = new BsonDocument("find", new BsonString("show")); - query1.put("stuff", new BsonString("secret")); - final BsonDocument query2 = new BsonDocument("insert", new BsonString("table")); - final BsonDocument query2_1 = new BsonDocument("count", new BsonString("show")); - query2_1.put("id", new BsonString("secret")); - query2.put("docs", new BsonArray(Arrays.asList(new BsonString("secret"), query2_1))); - final List queries = Arrays.asList(query1, query2); - for (final BsonDocument query : queries) { - final CommandStartedEvent cmd = - new CommandStartedEvent(1, makeConnection(), "databasename", "query", query); - - final DDSpan span = new DDTracer().buildSpan("foo").start(); - MongoClientDecorator.DECORATE.afterStart(span); - MongoClientDecorator.DECORATE.onStatement(span, cmd.getCommand()); - - assertThat(span.getSpanType()).isEqualTo(DDSpanTypes.MONGO); - assertThat(span.getTags().get(Tags.DB_STATEMENT.getKey())) - .isEqualTo(query.toString().replaceAll("secret", "?")); - } - } -} diff --git a/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle b/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle deleted file mode 100644 index df7028ecb5..0000000000 --- a/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle +++ /dev/null @@ -1,27 +0,0 @@ -muzzle { - pass { - group = "org.mongodb" - module = "mongodb-driver-async" - versions = "[3.3,)" - extraDependency 'org.mongodb:mongo-java-driver:3.3.0' - assertInverse = true - } -} - -apply from: "${rootDir}/gradle/java.gradle" - -dependencies { - // use mongo listener - compile(project(':dd-java-agent:instrumentation:mongo-3.1')) { - transitive = false - } - compileOnly group: 'org.mongodb', name: 'mongo-java-driver', version: '3.3.0' - compileOnly group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.3.0' - - compile project(':dd-java-agent:agent-tooling') - - compile deps.bytebuddy - compile deps.opentracing - annotationProcessor deps.autoservice - implementation deps.autoservice -} diff --git a/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle b/dd-java-agent/instrumentation/mongo/driver-3.1/driver-3.1.gradle similarity index 80% rename from dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle rename to dd-java-agent/instrumentation/mongo/driver-3.1/driver-3.1.gradle index 5f1a0d2a0a..747c30ad60 100644 --- a/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle +++ b/dd-java-agent/instrumentation/mongo/driver-3.1/driver-3.1.gradle @@ -28,9 +28,8 @@ dependencies { implementation deps.autoservice testCompile project(':dd-java-agent:testing') - - testCompile group: 'org.assertj', name: 'assertj-core', version: '2.9.+' - testCompile group: 'org.mockito', name: 'mockito-core', version: '2.19.0' + testCompile project(':dd-java-agent:instrumentation:mongo').sourceSets.test.output + testCompile group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5' testCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0' latestDepTestCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '+' diff --git a/dd-java-agent/instrumentation/mongo-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java b/dd-java-agent/instrumentation/mongo/driver-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java similarity index 77% rename from dd-java-agent/instrumentation/mongo-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java rename to dd-java-agent/instrumentation/mongo/driver-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java index 08474ce55b..a5d4782cdc 100644 --- a/dd-java-agent/instrumentation/mongo-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java +++ b/dd-java-agent/instrumentation/mongo/driver-3.1/src/main/java/datadog/trace/instrumentation/mongo/MongoClientDecorator.java @@ -1,5 +1,9 @@ package datadog.trace.instrumentation.mongo; +import com.mongodb.connection.ClusterId; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.connection.ConnectionId; +import com.mongodb.connection.ServerId; import com.mongodb.event.CommandStartedEvent; import datadog.trace.agent.decorator.DatabaseClientDecorator; import datadog.trace.api.DDSpanTypes; @@ -48,19 +52,25 @@ public class MongoClientDecorator extends DatabaseClientDecorator collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + collection.insertOne(new Document("password", "SECRET")) + + then: + collection.count() == 1 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0, "{\"insert\":\"$collectionName\",\"ordered\":\"?\",\"documents\":[{\"_id\":\"?\",\"password\":\"?\"}]}") + } + trace(1, 1) { + mongoSpan(it, 0, "{\"count\":\"$collectionName\",\"query\":{}}") + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test update"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "OLDPW")) + return coll + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + def result = collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW")))) + + then: + result.modifiedCount == 1 + collection.count() == 1 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0, "{\"update\":\"?\",\"ordered\":\"?\",\"updates\":[{\"q\":{\"password\":\"?\"},\"u\":{\"\$set\":{\"password\":\"?\"}}}]}") + } + trace(1, 1) { + mongoSpan(it, 0, "{\"count\":\"$collectionName\",\"query\":{}}") + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test delete"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "SECRET")) + return coll + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET"))) + + then: + result.deletedCount == 1 + collection.count() == 0 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0, "{\"delete\":\"?\",\"ordered\":\"?\",\"deletes\":[{\"q\":{\"password\":\"?\"},\"limit\":\"?\"}]}") + } + trace(1, 1) { + mongoSpan(it, 0, "{\"count\":\"$collectionName\",\"query\":{}}") + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test error"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + collection.updateOne(new BsonDocument(), new BsonDocument()) + + then: + thrown(IllegalArgumentException) + // Unfortunately not caught by our instrumentation. + assertTraces(0) {} + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test client failure"() { + setup: + def options = MongoClientOptions.builder().serverSelectionTimeout(10).build() + def client = new MongoClient(new ServerAddress("localhost", UNUSABLE_PORT), [], options) + + when: + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + + then: + thrown(MongoTimeoutException) + // Unfortunately not caught by our instrumentation. + assertTraces(0) {} + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def mongoSpan(TraceAssert trace, int index, String statement, String instance = "some-description", Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + serviceName "mongo" + operationName "mongo.query" + resourceName { + assert it.replace(" ", "") == statement + return true + } + spanType DDSpanTypes.MONGO + if (parentSpan == null) { + parent() + } else { + childOf((DDSpan) parentSpan) + } + tags { + "$Tags.COMPONENT.key" "java-mongo" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "$Tags.DB_INSTANCE.key" instance + "$Tags.DB_STATEMENT.key" { + it.replace(" ", "") == statement + } + "$Tags.DB_TYPE.key" "mongo" + "$Tags.PEER_HOSTNAME.key" "localhost" + "$Tags.PEER_HOST_IPV4.key" "127.0.0.1" + "$Tags.PEER_PORT.key" port + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/mongo/driver-async-3.3/driver-async-3.3.gradle b/dd-java-agent/instrumentation/mongo/driver-async-3.3/driver-async-3.3.gradle new file mode 100644 index 0000000000..0461926bfb --- /dev/null +++ b/dd-java-agent/instrumentation/mongo/driver-async-3.3/driver-async-3.3.gradle @@ -0,0 +1,47 @@ +// Set properties before any plugins get loaded +ext { + // Since we're using CompletableFutures in the test... + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +muzzle { + pass { + group = "org.mongodb" + module = "mongodb-driver-async" + versions = "[3.3,)" + extraDependency 'org.mongodb:mongo-java-driver:3.3.0' + assertInverse = true + } +} + +apply from: "${rootDir}/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +dependencies { + // use mongo listener + compile(project(':dd-java-agent:instrumentation:mongo:driver-3.1')) { + transitive = false + } + compileOnly group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.3.0' + + compile project(':dd-java-agent:agent-tooling') + + compile deps.bytebuddy + compile deps.opentracing + annotationProcessor deps.autoservice + implementation deps.autoservice + + testCompile project(':dd-java-agent:testing') + testCompile project(':dd-java-agent:instrumentation:mongo').sourceSets.test.output + testCompile group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5' + + testCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.3.0' + latestDepTestCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '+' +} diff --git a/dd-java-agent/instrumentation/mongo-async-3.3/src/main/java/datadog/trace/instrumentation/mongo/MongoAsyncClientInstrumentation.java b/dd-java-agent/instrumentation/mongo/driver-async-3.3/src/main/java/datadog/trace/instrumentation/mongo/MongoAsyncClientInstrumentation.java similarity index 100% rename from dd-java-agent/instrumentation/mongo-async-3.3/src/main/java/datadog/trace/instrumentation/mongo/MongoAsyncClientInstrumentation.java rename to dd-java-agent/instrumentation/mongo/driver-async-3.3/src/main/java/datadog/trace/instrumentation/mongo/MongoAsyncClientInstrumentation.java diff --git a/dd-java-agent/instrumentation/mongo/driver-async-3.3/src/test/groovy/MongoAsyncClientTest.groovy b/dd-java-agent/instrumentation/mongo/driver-async-3.3/src/test/groovy/MongoAsyncClientTest.groovy new file mode 100644 index 0000000000..1ba2cdc099 --- /dev/null +++ b/dd-java-agent/instrumentation/mongo/driver-async-3.3/src/test/groovy/MongoAsyncClientTest.groovy @@ -0,0 +1,298 @@ +import com.mongodb.ConnectionString +import com.mongodb.async.SingleResultCallback +import com.mongodb.async.client.MongoClient +import com.mongodb.async.client.MongoClientSettings +import com.mongodb.async.client.MongoClients +import com.mongodb.async.client.MongoCollection +import com.mongodb.async.client.MongoDatabase +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.connection.ClusterSettings +import datadog.opentracing.DDSpan +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.api.DDSpanTypes +import io.opentracing.tag.Tags +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import spock.lang.Shared +import spock.lang.Timeout + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch + +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +@Timeout(10) +class MongoAsyncClientTest extends MongoBaseTest { + + @Shared + MongoClient client + + def setup() throws Exception { + client = MongoClients.create( + MongoClientSettings.builder() + .clusterSettings( + ClusterSettings.builder() + .description("some-description") + .applyConnectionString(new ConnectionString("mongodb://localhost:$port")) + .build()) + .build()) + } + + def cleanup() throws Exception { + client?.close() + client = null + } + + def "test create collection"() { + setup: + MongoDatabase db = client.getDatabase(dbName) + + when: + db.createCollection(collectionName, toCallback {}) + + then: + assertTraces(1) { + trace(0, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" || + it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test create collection no description"() { + setup: + MongoDatabase db = MongoClients.create("mongodb://localhost:$port").getDatabase(dbName) + + when: + db.createCollection(collectionName, toCallback {}) + + then: + assertTraces(1) { + trace(0, 1) { + mongoSpan(it, 0, { + assert it.replaceAll(" ", "") == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" || + it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + }, dbName) + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test get collection"() { + setup: + MongoDatabase db = client.getDatabase(dbName) + + when: + def count = new CompletableFuture() + db.getCollection(collectionName).count toCallback { count.complete(it) } + + then: + count.get() == 0 + assertTraces(1) { + trace(0, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test insert"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + return db.getCollection(collectionName) + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + def count = new CompletableFuture() + collection.insertOne(new Document("password", "SECRET"), toCallback { + collection.count toCallback { count.complete(it) } + }) + + then: + count.get() == 1 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"insert\":\"$collectionName\",\"ordered\":\"?\",\"documents\":[{\"_id\":\"?\",\"password\":\"?\"}]}" || + it == "{\"insert\": \"$collectionName\", \"ordered\": \"?\", \"\$db\": \"?\", \"documents\": [{\"_id\": \"?\", \"password\": \"?\"}]}" + true + } + } + trace(1, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test update"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "OLDPW"), toCallback { latch2.countDown() }) + latch2.await() + return coll + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))), toCallback { + result.complete(it) + collection.count toCallback { count.complete(it) } + }) + + then: + result.get().modifiedCount == 1 + count.get() == 1 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"update\":\"?\",\"ordered\":\"?\",\"updates\":[{\"q\":{\"password\":\"?\"},\"u\":{\"\$set\":{\"password\":\"?\"}}}]}" || + it == "{\"update\": \"?\", \"ordered\": \"?\", \"\$db\": \"?\", \"updates\": [{\"q\": {\"password\": \"?\"}, \"u\": {\"\$set\": {\"password\": \"?\"}}}]}" + true + } + } + trace(1, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + def "test delete"() { + setup: + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "SECRET"), toCallback { latch2.countDown() }) + latch2.await() + return coll + } + TEST_WRITER.waitForTraces(1) + TEST_WRITER.clear() + + when: + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")), toCallback { + result.complete(it) + collection.count toCallback { count.complete(it) } + }) + + then: + result.get().deletedCount == 1 + count.get() == 0 + assertTraces(2) { + trace(0, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"delete\":\"?\",\"ordered\":\"?\",\"deletes\":[{\"q\":{\"password\":\"?\"},\"limit\":\"?\"}]}" || + it == "{\"delete\": \"?\", \"ordered\": \"?\", \"\$db\": \"?\", \"deletes\": [{\"q\": {\"password\": \"?\"}, \"limit\": \"?\"}]}" + true + } + } + trace(1, 1) { + mongoSpan(it, 0) { + assert it.replaceAll(" ", "") == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = "testCollection" + } + + SingleResultCallback toCallback(Closure closure) { + return new SingleResultCallback() { + @Override + void onResult(Object result, Throwable t) { + if (t) { + closure.call(t) + } else { + closure.call(result) + } + } + } + } + + def mongoSpan(TraceAssert trace, int index, Closure statementEval, String instance = "some-description", Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + serviceName "mongo" + operationName "mongo.query" + resourceName statementEval + spanType DDSpanTypes.MONGO + if (parentSpan == null) { + parent() + } else { + childOf((DDSpan) parentSpan) + } + tags { + "$Tags.COMPONENT.key" "java-mongo" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "$Tags.DB_INSTANCE.key" instance + "$Tags.DB_STATEMENT.key" statementEval + "$Tags.DB_TYPE.key" "mongo" + "$Tags.PEER_HOSTNAME.key" "localhost" + "$Tags.PEER_HOST_IPV4.key" "127.0.0.1" + "$Tags.PEER_PORT.key" port + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/mongo/mongo.gradle b/dd-java-agent/instrumentation/mongo/mongo.gradle new file mode 100644 index 0000000000..f91b66b0cc --- /dev/null +++ b/dd-java-agent/instrumentation/mongo/mongo.gradle @@ -0,0 +1,28 @@ +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + testAnnotationProcessor deps.autoservice + testImplementation deps.autoservice + + testCompile project(':dd-java-agent:testing') + testCompile group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5' +} + +// Forcing strict test execution order (no parallel execution) to ensure proper mongo executable initialization. +List testTasks = [] +tasks.withType(Test) { Test testTask -> + testTasks.each { + testTask.shouldRunAfter(it) + } + testTasks.add(testTask) +} +subprojects { + afterEvaluate { + tasks.withType(Test) { Test testTask -> + testTasks.each { + testTask.shouldRunAfter(it) + } + testTasks.add(testTask) + } + } +} diff --git a/dd-java-agent/instrumentation/mongo/src/test/groovy/MongoBaseTest.groovy b/dd-java-agent/instrumentation/mongo/src/test/groovy/MongoBaseTest.groovy new file mode 100644 index 0000000000..c945733e35 --- /dev/null +++ b/dd-java-agent/instrumentation/mongo/src/test/groovy/MongoBaseTest.groovy @@ -0,0 +1,54 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.agent.test.utils.PortUtils +import de.flapdoodle.embed.mongo.MongodExecutable +import de.flapdoodle.embed.mongo.MongodProcess +import de.flapdoodle.embed.mongo.MongodStarter +import de.flapdoodle.embed.mongo.config.IMongodConfig +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder +import de.flapdoodle.embed.mongo.config.Net +import de.flapdoodle.embed.mongo.distribution.Version +import de.flapdoodle.embed.process.runtime.Network +import spock.lang.Shared + +/** + * Testing needs to be in a centralized project. + * If tests in multiple different projects are using embedded mongo, + * they downloader is at risk of a race condition. + */ +class MongoBaseTest extends AgentTestRunner { + // https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo#executable-collision + private static final MongodStarter STARTER = MongodStarter.getDefaultInstance() + + @Shared + int port = PortUtils.randomOpenPort() + @Shared + MongodExecutable mongodExe + @Shared + MongodProcess mongod + + def setup() throws Exception { + final IMongodConfig mongodConfig = + new MongodConfigBuilder() + .version(Version.Main.PRODUCTION) + .net(new Net("localhost", port, Network.localhostIsIPv6())) + .build() + + mongodExe = STARTER.prepare(mongodConfig) + mongod = mongodExe.start() + } + + def cleanup() throws Exception { + mongod?.stop() + mongod = null + mongodExe?.stop() + mongodExe = null + } + + def "test port open"() { + when: + new Socket("localhost", port) + + then: + noExceptionThrown() + } +} diff --git a/dd-java-agent/instrumentation/mongo/src/test/java/NoOpInstrumentation.java b/dd-java-agent/instrumentation/mongo/src/test/java/NoOpInstrumentation.java new file mode 100644 index 0000000000..d830b1adf3 --- /dev/null +++ b/dd-java-agent/instrumentation/mongo/src/test/java/NoOpInstrumentation.java @@ -0,0 +1,12 @@ +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.agent.builder.AgentBuilder; + +@AutoService(Instrumenter.class) +public class NoOpInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(final AgentBuilder agentBuilder) { + return agentBuilder; + } +} diff --git a/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ClientTest.groovy b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ClientTest.groovy index 0961415733..cdf7c74d77 100644 --- a/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ClientTest.groovy +++ b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ClientTest.groovy @@ -9,6 +9,7 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static org.asynchttpclient.Dsl.asyncHttpClient @@ -65,7 +66,7 @@ class Netty40ClientTest extends HttpClientTest { and: assertTraces(1) { trace(0, 2) { - parentSpan(it, 0, thrownException) + basicSpan(it, 0, "parent", thrownException) span(1) { operationName "netty.connect" diff --git a/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ClientTest.groovy b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ClientTest.groovy index e8f722e899..0a76910b66 100644 --- a/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ClientTest.groovy +++ b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ClientTest.groovy @@ -10,6 +10,7 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static org.asynchttpclient.Dsl.asyncHttpClient @@ -67,7 +68,7 @@ class Netty41ClientTest extends HttpClientTest { and: assertTraces(1) { trace(0, 2) { - parentSpan(it, 0, thrownException) + basicSpan(it, 0, "parent", thrownException) span(1) { operationName "netty.connect" diff --git a/dd-java-agent/instrumentation/spymemcached-2.12/src/main/java/datadog/trace/instrumentation/spymemcached/MemcacheClientDecorator.java b/dd-java-agent/instrumentation/spymemcached-2.12/src/main/java/datadog/trace/instrumentation/spymemcached/MemcacheClientDecorator.java index f5729f94c6..cbb51e6784 100644 --- a/dd-java-agent/instrumentation/spymemcached-2.12/src/main/java/datadog/trace/instrumentation/spymemcached/MemcacheClientDecorator.java +++ b/dd-java-agent/instrumentation/spymemcached-2.12/src/main/java/datadog/trace/instrumentation/spymemcached/MemcacheClientDecorator.java @@ -41,7 +41,7 @@ public class MemcacheClientDecorator extends DatabaseClientDecorator() { - @Override - public void onResult(final Void result, final Throwable t) { - done.set(true); - } - }); - while (!done.get()) { - Thread.sleep(1); - } - - db.getCollection(collectionName) - .insertOne( - new Document("foo", "bar"), - new SingleResultCallback() { - @Override - public void onResult(final Void result, final Throwable t) { - done.set(true); - } - }); - while (!done.get()) { - Thread.sleep(1); - } - - done.set(false); - db.getCollection(collectionName) - .count( - new SingleResultCallback() { - @Override - public void onResult(final Long result, final Throwable t) { - Assert.assertEquals(1, result.longValue()); - done.set(true); - } - }); - - while (!done.get()) { - Thread.sleep(1); - } - - // the final trace may still be reporting to the ListWriter, - // but we're only testing the first trace. - Assert.assertTrue(writer.size() >= 1); - - final String createCollectionQuery = - "{ \"create\" : \"asyncCollection\", \"autoIndexId\" : \"?\", \"capped\" : \"?\" }"; - final DDSpan trace0 = writer.get(0).get(0); - Assert.assertEquals("mongo.query", trace0.getOperationName()); - Assert.assertEquals(createCollectionQuery, trace0.getResourceName()); - Assert.assertEquals("mongodb", trace0.getType()); - Assert.assertEquals("mongo", trace0.getServiceName()); - - Assert.assertEquals("java-mongo", trace0.getTags().get(Tags.COMPONENT.getKey())); - Assert.assertEquals(createCollectionQuery, trace0.getTags().get(Tags.DB_STATEMENT.getKey())); - Assert.assertEquals(MONGO_DB_NAME, trace0.getTags().get(Tags.DB_INSTANCE.getKey())); - Assert.assertEquals(MONGO_HOST, trace0.getTags().get(Tags.PEER_HOSTNAME.getKey())); - Assert.assertEquals("127.0.0.1", trace0.getTags().get(Tags.PEER_HOST_IPV4.getKey())); - Assert.assertEquals(MONGO_PORT, trace0.getTags().get(Tags.PEER_PORT.getKey())); - Assert.assertEquals("mongo", trace0.getTags().get(Tags.DB_TYPE.getKey())); - } -} diff --git a/dd-java-agent/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java b/dd-java-agent/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java deleted file mode 100644 index 9942c6e535..0000000000 --- a/dd-java-agent/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package datadog.trace.agent.integration; - -import com.mongodb.MongoClient; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; -import datadog.opentracing.DDSpan; -import datadog.opentracing.DDTracer; -import datadog.trace.agent.test.IntegrationTestUtils; -import datadog.trace.common.writer.ListWriter; -import de.flapdoodle.embed.mongo.MongodExecutable; -import de.flapdoodle.embed.mongo.MongodProcess; -import de.flapdoodle.embed.mongo.MongodStarter; -import de.flapdoodle.embed.mongo.config.IMongodConfig; -import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; -import de.flapdoodle.embed.mongo.config.Net; -import de.flapdoodle.embed.mongo.distribution.Version; -import de.flapdoodle.embed.process.runtime.Network; -import io.opentracing.tag.Tags; -import java.util.concurrent.TimeoutException; -import org.bson.Document; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; - -public class MongoClientInstrumentationTest { - public static final String MONGO_DB_NAME = "embedded"; - public static final String MONGO_HOST = "localhost"; - public static final int MONGO_PORT = 12345; - private static MongodExecutable mongodExe; - private static MongodProcess mongod; - - private static MongoClient client; - private static final ListWriter writer = new ListWriter(); - private static final DDTracer tracer = new DDTracer(writer); - - public static void startLocalMongo() throws Exception { - final MongodStarter starter = MongodStarter.getDefaultInstance(); - final IMongodConfig mongodConfig = - new MongodConfigBuilder() - .version(Version.Main.PRODUCTION) - .net(new Net(MONGO_HOST, MONGO_PORT, Network.localhostIsIPv6())) - .build(); - - mongodExe = starter.prepare(mongodConfig); - mongod = mongodExe.start(); - } - - public static void stopLocalMongo() throws Exception { - if (null != mongod) { - mongod.stop(); - mongod = null; - } - if (null != mongodExe) { - mongodExe.stop(); - mongodExe = null; - } - } - - @BeforeClass - public static void setup() throws Exception { - IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer); - startLocalMongo(); - // Embeded Mongo uses HttpUrlConnection to download things so we have to clear traces before - // going to tests - writer.clear(); - - client = new MongoClient(MONGO_HOST, MONGO_PORT); - } - - @AfterClass - public static void destroy() throws Exception { - if (null != client) { - client.close(); - client = null; - } - stopLocalMongo(); - } - - @Test - public void syncClientHasListener() { - Assert.assertEquals(1, client.getMongoClientOptions().getCommandListeners().size()); - Assert.assertEquals( - "TracingCommandListener", - client.getMongoClientOptions().getCommandListeners().get(0).getClass().getSimpleName()); - } - - @Test - public void insertOperation() throws TimeoutException, InterruptedException { - final MongoDatabase db = client.getDatabase(MONGO_DB_NAME); - final String collectionName = "testCollection"; - db.createCollection(collectionName); - final MongoCollection collection = db.getCollection(collectionName); - - collection.insertOne(new Document("foo", "bar")); - - Assert.assertEquals(1, collection.count()); - - Assert.assertEquals(3, writer.size()); - - final String createCollectionQuery = - "{ \"create\" : \"testCollection\", \"autoIndexId\" : \"?\", \"capped\" : \"?\" }"; - final DDSpan trace0 = writer.get(0).get(0); - Assert.assertEquals("mongo.query", trace0.getOperationName()); - Assert.assertEquals(createCollectionQuery, trace0.getResourceName()); - Assert.assertEquals("mongodb", trace0.getType()); - Assert.assertEquals("mongo", trace0.getServiceName()); - - Assert.assertEquals("java-mongo", trace0.getTags().get(Tags.COMPONENT.getKey())); - Assert.assertEquals(createCollectionQuery, trace0.getTags().get(Tags.DB_STATEMENT.getKey())); - Assert.assertEquals(MONGO_DB_NAME, trace0.getTags().get(Tags.DB_INSTANCE.getKey())); - Assert.assertEquals(MONGO_HOST, trace0.getTags().get(Tags.PEER_HOSTNAME.getKey())); - Assert.assertEquals("127.0.0.1", trace0.getTags().get(Tags.PEER_HOST_IPV4.getKey())); - Assert.assertEquals(MONGO_PORT, trace0.getTags().get(Tags.PEER_PORT.getKey())); - Assert.assertEquals("mongo", trace0.getTags().get(Tags.DB_TYPE.getKey())); - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy index 319d6916e1..eae9c17550 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/SpanAssert.groovy @@ -4,6 +4,8 @@ import datadog.opentracing.DDSpan import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType +import java.util.regex.Pattern + import static TagsAssert.assertTags class SpanAssert { @@ -52,11 +54,21 @@ class SpanAssert { checked.operationName = true } + def resourceName(Pattern pattern) { + assert span.resourceName.matches(pattern) + checked.resourceName = true + } + def resourceName(String name) { assert span.resourceName == name checked.resourceName = true } + def resourceName(Closure eval) { + assert eval(span.resourceName) + checked.resourceName = true + } + def resourceNameContains(String... resourceNameParts) { assertSpanNameContains(span.resourceName, resourceNameParts) checked.resourceName = true diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy index 5bc4818091..cc949da32c 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy @@ -16,6 +16,7 @@ import java.util.concurrent.ExecutionException import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer import static datadog.trace.agent.test.utils.ConfigUtils.withConfigOverride import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static org.junit.Assume.assumeTrue @@ -108,7 +109,7 @@ abstract class HttpClientTest extends AgentTestRu assertTraces(2) { server.distributedRequestTrace(it, 0, trace(1).last()) trace(1, size(2)) { - parentSpan(it, 0) + basicSpan(it, 0, "parent") clientSpan(it, 1, span(0), method, false) } } @@ -150,7 +151,7 @@ abstract class HttpClientTest extends AgentTestRu // only one trace (client). assertTraces(1) { trace(0, size(2)) { - parentSpan(it, 0) + basicSpan(it, 0, "parent") clientSpan(it, 1, span(0), method, renameService) } } @@ -173,7 +174,7 @@ abstract class HttpClientTest extends AgentTestRu // only one trace (client). assertTraces(1) { trace(0, size(3)) { - parentSpan(it, 0) + basicSpan(it, 0, "parent") span(1) { operationName "child" childOf span(0) @@ -304,7 +305,7 @@ abstract class HttpClientTest extends AgentTestRu and: assertTraces(1) { trace(0, 2) { - parentSpan(it, 0, thrownException) + basicSpan(it, 0, "parent", thrownException) clientSpan(it, 1, span(0), method, false, false, uri, null, thrownException) } } @@ -313,22 +314,6 @@ abstract class HttpClientTest extends AgentTestRu method = "GET" } - void parentSpan(TraceAssert trace, int index, Throwable exception = null) { - trace.span(index) { - parent() - serviceName "unnamed-java-app" - operationName "parent" - resourceName "parent" - errored exception != null - tags { - defaultTags() - if (exception) { - errorTags(exception.class, exception.message) - } - } - } - } - // parent span must be cast otherwise it breaks debugging classloading (junit loads it early) void clientSpan(TraceAssert trace, int index, Object parentSpan, String method = "GET", boolean renameService = false, boolean tagQueryString = false, URI uri = server.address.resolve("/success"), Integer status = 200, Throwable exception = null) { trace.span(index) { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy index db2dc132fc..c349504cb1 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy @@ -1,6 +1,7 @@ package datadog.trace.agent.test.utils import datadog.trace.agent.decorator.BaseDecorator +import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.context.TraceScope import io.opentracing.Scope import io.opentracing.util.GlobalTracer @@ -40,4 +41,20 @@ class TraceUtils { scope.close() } } + + static basicSpan(TraceAssert trace, int index, String spanName, Throwable exception = null) { + trace.span(index) { + parent() + serviceName "unnamed-java-app" + operationName spanName + resourceName spanName + errored exception != null + tags { + defaultTags() + if (exception) { + errorTags(exception.class, exception.message) + } + } + } + } } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/Config.java b/dd-trace-api/src/main/java/datadog/trace/api/Config.java index b334728ee1..7ea7e7cec3 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/Config.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/Config.java @@ -68,6 +68,7 @@ public class Config { public static final String HTTP_SERVER_TAG_QUERY_STRING = "http.server.tag.query-string"; public static final String HTTP_CLIENT_TAG_QUERY_STRING = "http.client.tag.query-string"; public static final String HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN = "trace.http.client.split-by-domain"; + public static final String DB_CLIENT_HOST_SPLIT_BY_INSTANCE = "trace.db.client.split-by-instance"; public static final String PARTIAL_FLUSH_MIN_SPANS = "trace.partial.flush.min.spans"; public static final String RUNTIME_CONTEXT_FIELD_INJECTION = "trace.runtime.context.field.injection"; @@ -112,6 +113,7 @@ public class Config { private static final boolean DEFAULT_HTTP_SERVER_TAG_QUERY_STRING = false; private static final boolean DEFAULT_HTTP_CLIENT_TAG_QUERY_STRING = false; private static final boolean DEFAULT_HTTP_CLIENT_SPLIT_BY_DOMAIN = false; + private static final boolean DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE = false; private static final int DEFAULT_PARTIAL_FLUSH_MIN_SPANS = 1000; private static final String DEFAULT_PROPAGATION_STYLE_EXTRACT = PropagationStyle.DATADOG.name(); private static final String DEFAULT_PROPAGATION_STYLE_INJECT = PropagationStyle.DATADOG.name(); @@ -165,6 +167,7 @@ public class Config { @Getter private final boolean httpServerTagQueryString; @Getter private final boolean httpClientTagQueryString; @Getter private final boolean httpClientSplitByDomain; + @Getter private final boolean dbClientSplitByInstance; @Getter private final Integer partialFlushMinSpans; @Getter private final boolean runtimeContextFieldInjection; @Getter private final Set propagationStylesToExtract; @@ -248,6 +251,10 @@ public class Config { getBooleanSettingFromEnvironment( HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, DEFAULT_HTTP_CLIENT_SPLIT_BY_DOMAIN); + dbClientSplitByInstance = + getBooleanSettingFromEnvironment( + DB_CLIENT_HOST_SPLIT_BY_INSTANCE, DEFAULT_DB_CLIENT_HOST_SPLIT_BY_INSTANCE); + partialFlushMinSpans = getIntegerSettingFromEnvironment(PARTIAL_FLUSH_MIN_SPANS, DEFAULT_PARTIAL_FLUSH_MIN_SPANS); @@ -352,6 +359,10 @@ public class Config { getPropertyBooleanValue( properties, HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, parent.httpClientSplitByDomain); + dbClientSplitByInstance = + getPropertyBooleanValue( + properties, DB_CLIENT_HOST_SPLIT_BY_INSTANCE, parent.dbClientSplitByInstance); + partialFlushMinSpans = getPropertyIntegerValue(properties, PARTIAL_FLUSH_MIN_SPANS, parent.partialFlushMinSpans); diff --git a/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index 6937268c91..f9fd7f3894 100644 --- a/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -9,6 +9,7 @@ import static datadog.trace.api.Config.AGENT_HOST import static datadog.trace.api.Config.AGENT_PORT_LEGACY import static datadog.trace.api.Config.AGENT_UNIX_DOMAIN_SOCKET import static datadog.trace.api.Config.CONFIGURATION_FILE +import static datadog.trace.api.Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE import static datadog.trace.api.Config.DEFAULT_JMX_FETCH_STATSD_PORT import static datadog.trace.api.Config.GLOBAL_TAGS import static datadog.trace.api.Config.HEADER_TAGS @@ -80,6 +81,7 @@ class ConfigTest extends Specification { config.httpServerErrorStatuses == (500..599).toSet() config.httpClientErrorStatuses == (400..499).toSet() config.httpClientSplitByDomain == false + config.dbClientSplitByInstance == false config.partialFlushMinSpans == 1000 config.reportHostName == false config.runtimeContextFieldInjection == true @@ -121,6 +123,7 @@ class ConfigTest extends Specification { prop.setProperty(HTTP_SERVER_ERROR_STATUSES, "123-456,457,124-125,122") prop.setProperty(HTTP_CLIENT_ERROR_STATUSES, "111") prop.setProperty(HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") + prop.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") prop.setProperty(PARTIAL_FLUSH_MIN_SPANS, "15") prop.setProperty(TRACE_REPORT_HOSTNAME, "true") prop.setProperty(RUNTIME_CONTEXT_FIELD_INJECTION, "false") @@ -152,6 +155,7 @@ class ConfigTest extends Specification { config.httpServerErrorStatuses == (122..457).toSet() config.httpClientErrorStatuses == (111..111).toSet() config.httpClientSplitByDomain == true + config.dbClientSplitByInstance == true config.partialFlushMinSpans == 15 config.reportHostName == true config.runtimeContextFieldInjection == false @@ -184,6 +188,7 @@ class ConfigTest extends Specification { System.setProperty(PREFIX + HTTP_SERVER_ERROR_STATUSES, "123-456,457,124-125,122") System.setProperty(PREFIX + HTTP_CLIENT_ERROR_STATUSES, "111") System.setProperty(PREFIX + HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") + System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") System.setProperty(PREFIX + PARTIAL_FLUSH_MIN_SPANS, "25") System.setProperty(PREFIX + TRACE_REPORT_HOSTNAME, "true") System.setProperty(PREFIX + RUNTIME_CONTEXT_FIELD_INJECTION, "false") @@ -215,6 +220,7 @@ class ConfigTest extends Specification { config.httpServerErrorStatuses == (122..457).toSet() config.httpClientErrorStatuses == (111..111).toSet() config.httpClientSplitByDomain == true + config.dbClientSplitByInstance == true config.partialFlushMinSpans == 25 config.reportHostName == true config.runtimeContextFieldInjection == false @@ -288,6 +294,7 @@ class ConfigTest extends Specification { System.setProperty(PREFIX + HTTP_SERVER_ERROR_STATUSES, "1111") System.setProperty(PREFIX + HTTP_CLIENT_ERROR_STATUSES, "1:1") System.setProperty(PREFIX + HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "invalid") + System.setProperty(PREFIX + DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "invalid") System.setProperty(PREFIX + PROPAGATION_STYLE_EXTRACT, "some garbage") System.setProperty(PREFIX + PROPAGATION_STYLE_INJECT, " ") @@ -308,6 +315,7 @@ class ConfigTest extends Specification { config.httpServerErrorStatuses == (500..599).toSet() config.httpClientErrorStatuses == (400..499).toSet() config.httpClientSplitByDomain == false + config.dbClientSplitByInstance == false config.propagationStylesToExtract.toList() == [Config.PropagationStyle.DATADOG] config.propagationStylesToInject.toList() == [Config.PropagationStyle.DATADOG] } @@ -373,6 +381,7 @@ class ConfigTest extends Specification { properties.setProperty(HTTP_SERVER_ERROR_STATUSES, "123-456,457,124-125,122") properties.setProperty(HTTP_CLIENT_ERROR_STATUSES, "111") properties.setProperty(HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") + properties.setProperty(DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") properties.setProperty(PARTIAL_FLUSH_MIN_SPANS, "15") properties.setProperty(PROPAGATION_STYLE_EXTRACT, "B3 Datadog") properties.setProperty(PROPAGATION_STYLE_INJECT, "Datadog B3") @@ -401,6 +410,7 @@ class ConfigTest extends Specification { config.httpServerErrorStatuses == (122..457).toSet() config.httpClientErrorStatuses == (111..111).toSet() config.httpClientSplitByDomain == true + config.dbClientSplitByInstance == true config.partialFlushMinSpans == 15 config.propagationStylesToExtract.toList() == [Config.PropagationStyle.B3, Config.PropagationStyle.DATADOG] config.propagationStylesToInject.toList() == [Config.PropagationStyle.DATADOG, Config.PropagationStyle.B3] diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index f7b1741862..00d9a61366 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -7,7 +7,7 @@ ext { slf4j : "1.7.25", guava : "20.0", // Last version to support Java 7 - jackson : "2.9.8", // https://nvd.nist.gov/vuln/detail/CVE-2018-1000873 + jackson : "2.9.9", // https://nvd.nist.gov/vuln/detail/CVE-2019-12086 spock : "1.3-groovy-$spockGroovyVer", groovy : groovyVer, diff --git a/settings.gradle b/settings.gradle index d31e0b382d..0cd07e4c11 100644 --- a/settings.gradle +++ b/settings.gradle @@ -66,8 +66,9 @@ include ':dd-java-agent:instrumentation:jsp-2.3' include ':dd-java-agent:instrumentation:kafka-clients-0.11' include ':dd-java-agent:instrumentation:kafka-streams-0.11' include ':dd-java-agent:instrumentation:lettuce-5' -include ':dd-java-agent:instrumentation:mongo-3.1' -include ':dd-java-agent:instrumentation:mongo-async-3.3' +include ':dd-java-agent:instrumentation:mongo' +include ':dd-java-agent:instrumentation:mongo:driver-3.1' +include ':dd-java-agent:instrumentation:mongo:driver-async-3.3' include ':dd-java-agent:instrumentation:netty-4.0' include ':dd-java-agent:instrumentation:netty-4.1' include ':dd-java-agent:instrumentation:okhttp-3'