diff --git a/dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle b/dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle index 0aeb5d3960..14abf5f1d5 100644 --- a/dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle +++ b/dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle @@ -43,13 +43,22 @@ muzzle { } } +configurations { + testArtifacts +} + +// Create test artifacts so couchbase-2.6 can reuse tests +artifacts { + testArtifacts testJar +} + dependencies { compile project(':dd-java-agent:instrumentation:rxjava-1') - - compileOnly group: 'com.couchbase.client', name: 'java-client', version: '2.0.0' - - testCompile group: 'com.couchbase.mock', name: 'CouchbaseMock', version: '1.5.19' + compileOnly group: 'com.couchbase.client', name: 'java-client', version: '2.0.0' + + testCompile group: 'com.couchbase.mock', name: 'CouchbaseMock', version: '1.5.19' + testCompile group: 'org.springframework.data', name: 'spring-data-couchbase', version: '2.0.0.RELEASE' // Earliest version that seems to allow queries with CouchbaseMock: testCompile group: 'com.couchbase.client', name: 'java-client', version: '2.5.0' diff --git a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseAsyncClientTest.groovy b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseAsyncClientTest.groovy index 5b0d2b9862..b15dd9df1d 100644 --- a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseAsyncClientTest.groovy +++ b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseAsyncClientTest.groovy @@ -4,6 +4,7 @@ import com.couchbase.client.java.document.JsonDocument import com.couchbase.client.java.document.json.JsonObject import com.couchbase.client.java.env.CouchbaseEnvironment import com.couchbase.client.java.query.N1qlQuery +import spock.lang.Unroll import spock.util.concurrent.BlockingVariable import util.AbstractCouchbaseTest @@ -12,6 +13,7 @@ import java.util.concurrent.TimeUnit import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +@Unroll class CouchbaseAsyncClientTest extends AbstractCouchbaseTest { static final int TIMEOUT = 10 diff --git a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy index 62f901b6f1..ca0dfb9486 100644 --- a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy +++ b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy @@ -5,11 +5,13 @@ import com.couchbase.client.java.document.JsonDocument import com.couchbase.client.java.document.json.JsonObject import com.couchbase.client.java.env.CouchbaseEnvironment import com.couchbase.client.java.query.N1qlQuery +import spock.lang.Unroll import util.AbstractCouchbaseTest import static datadog.trace.agent.test.utils.TraceUtils.basicSpan import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace +@Unroll class CouchbaseClientTest extends AbstractCouchbaseTest { def "test hasBucket #type"() { when: @@ -41,44 +43,6 @@ class CouchbaseClientTest extends AbstractCouchbaseTest { // Connect to the bucket and open it Bucket bkt = cluster.openBucket(bucketSettings.name(), bucketSettings.password()) - // Create a JSON document and store it with the ID "helloworld" - JsonObject content = JsonObject.create().put("hello", "world") - def inserted = bkt.upsert(JsonDocument.create("helloworld", content)) - def found = bkt.get("helloworld") - - then: - found == inserted - found.content().getString("hello") == "world" - - sortAndAssertTraces(3) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Cluster.openBucket") - } - trace(1, 1) { - assertCouchbaseCall(it, 0, "Bucket.upsert", bucketSettings.name()) - } - trace(2, 1) { - assertCouchbaseCall(it, 0, "Bucket.get", bucketSettings.name()) - } - } - - cleanup: - cluster?.disconnect() - environment.shutdown() - - where: - bucketSettings << [bucketCouchbase, bucketMemcache] - - environment = envBuilder(bucketSettings).build() - cluster = CouchbaseCluster.create(environment, Arrays.asList("127.0.0.1")) - type = bucketSettings.type().name() - } - - def "test upsert and get #type under trace"() { - when: - // Connect to the bucket and open it - Bucket bkt = cluster.openBucket(bucketSettings.name(), bucketSettings.password()) - // Create a JSON document and store it with the ID "helloworld" JsonObject content = JsonObject.create().put("hello", "world") diff --git a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy index 19c210f368..6e23d0257a 100644 --- a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy +++ b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy @@ -9,10 +9,15 @@ import org.springframework.context.ConfigurableApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.data.repository.CrudRepository import spock.lang.Shared +import spock.lang.Unroll import util.AbstractCouchbaseTest +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +@Unroll class CouchbaseSpringRepositoryTest extends AbstractCouchbaseTest { - private static final Closure FIND + static final Closure FIND static { // This method is different in Spring Data 2+ try { @@ -68,6 +73,10 @@ class CouchbaseSpringRepositoryTest extends AbstractCouchbaseTest { TEST_WRITER.clear() } + def cleanup() { + repo.deleteAll() + } + def "test empty repo"() { when: def result = repo.findAll() @@ -81,70 +90,94 @@ class CouchbaseSpringRepositoryTest extends AbstractCouchbaseTest { assertCouchbaseCall(it, 0, "Bucket.query", bucketCouchbase.name()) } } - - where: - indexName = "test-index" } - def "test CRUD"() { - when: + def "test save"() { + setup: def doc = new Doc() - then: // CREATE - repo.save(doc) == doc - - and: - assertTraces(1) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.upsert", bucketCouchbase.name()) - } - } - TEST_WRITER.clear() - - and: // RETRIEVE - FIND(repo, "1") == doc - - and: - assertTraces(1) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.get", bucketCouchbase.name()) - } - } - TEST_WRITER.clear() - when: - doc.data = "other data" - - then: // UPDATE - repo.save(doc) == doc - repo.findAll().asList() == [doc] - - assertTraces(3) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.upsert", bucketCouchbase.name()) - } - trace(1, 1) { - assertCouchbaseCall(it, 0, "Bucket.query", bucketCouchbase.name()) - } - trace(2, 1) { - assertCouchbaseCall(it, 0, "Bucket.get", bucketCouchbase.name()) - } - } - TEST_WRITER.clear() - - when: // DELETE - repo.delete("1") + def result = repo.save(doc) then: - !repo.findAll().iterator().hasNext() - - and: - assertTraces(2) { + result == doc + assertTraces(1) { trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.remove", bucketCouchbase.name()) + assertCouchbaseCall(it, 0, "Bucket.upsert", bucketCouchbase.name()) } - trace(1, 1) { - assertCouchbaseCall(it, 0, "Bucket.query", bucketCouchbase.name()) + } + } + + def "test save and retrieve"() { + setup: + def doc = new Doc() + def result + + when: + runUnderTrace("someTrace") { + repo.save(doc) + result = FIND(repo, "1") + + blockUntilChildSpansFinished(2) + } + + then: // RETRIEVE + result == doc + sortAndAssertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 2, "Bucket.upsert", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 1, "Bucket.get", bucketCouchbase.name(), span(0)) + } + } + } + + def "test save and update"() { + setup: + def doc = new Doc() + + when: + runUnderTrace("someTrace") { + repo.save(doc) + doc.data = "other data" + repo.save(doc) + + blockUntilChildSpansFinished(2) + } + + + then: + sortAndAssertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 2, "Bucket.upsert", bucketCouchbase.name(), span(0)) + } + } + } + + def "save and delete"() { + setup: + def doc = new Doc() + def result + + when: // DELETE + runUnderTrace("someTrace") { + repo.save(doc) + repo.delete("1") + result = repo.findAll().iterator().hasNext() + + blockUntilChildSpansFinished(3) + } + + then: + assert !result + sortAndAssertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 3, "Bucket.upsert", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 2, "Bucket.remove", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 1, "Bucket.query", bucketCouchbase.name(), span(0)) } } } diff --git a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy index ead64dbab1..4e18ad8099 100644 --- a/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy +++ b/dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy @@ -7,8 +7,13 @@ import com.couchbase.client.java.cluster.ClusterManager import com.couchbase.client.java.env.CouchbaseEnvironment import org.springframework.data.couchbase.core.CouchbaseTemplate import spock.lang.Shared +import spock.lang.Unroll import util.AbstractCouchbaseTest +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +@Unroll class CouchbaseSpringTemplateTest extends AbstractCouchbaseTest { @Shared @@ -51,19 +56,25 @@ class CouchbaseSpringTemplateTest extends AbstractCouchbaseTest { def "test write #name"() { setup: def doc = new Doc() + def result when: - template.save(doc) - def result = template.findById("1", Doc) + runUnderTrace("someTrace") { + template.save(doc) + result = template.findById("1", Doc) + + blockUntilChildSpansFinished(2) + } + then: result != null - assertTraces(2) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.upsert", name) - } - trace(1, 1) { - assertCouchbaseCall(it, 0, "Bucket.get", name) + + sortAndAssertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 2, "Bucket.upsert", name, span(0)) + assertCouchbaseCall(it, 1, "Bucket.get", name, span(0)) } } @@ -77,16 +88,20 @@ class CouchbaseSpringTemplateTest extends AbstractCouchbaseTest { def doc = new Doc() when: - template.save(doc) - template.remove(doc) + runUnderTrace("someTrace") { + template.save(doc) + template.remove(doc) + + blockUntilChildSpansFinished(2) + } + then: - assertTraces(2) { - trace(0, 1) { - assertCouchbaseCall(it, 0, "Bucket.upsert", name) - } - trace(1, 1) { - assertCouchbaseCall(it, 0, "Bucket.remove", name) + sortAndAssertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 2, "Bucket.upsert", name, span(0)) + assertCouchbaseCall(it, 1, "Bucket.remove", name, span(0)) } } diff --git a/dd-java-agent/instrumentation/couchbase-2.6/couchbase-2.6.gradle b/dd-java-agent/instrumentation/couchbase-2.6/couchbase-2.6.gradle new file mode 100644 index 0000000000..92464fe3b8 --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/couchbase-2.6.gradle @@ -0,0 +1,63 @@ +// Set properties before any plugins get loaded +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "${rootDir}/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +muzzle { + // Version 2.7.5 and 2.7.8 were not released properly and muzzle cannot test against it causing failure. + // So we have to skip them resulting in this verbose setup. + fail { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[,2.6.0)" + } + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.6.0,2.7.5)" + } + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.7.6,2.7.8)" + } + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.7.9,)" + } + fail { + group = 'com.couchbase.client' + module = 'couchbase-client' + versions = "(,)" + } +} + +dependencies { + compile project(':dd-java-agent:instrumentation:rxjava-1') + + compileOnly group: 'com.couchbase.client', name: 'java-client', version: '2.6.0' + + testCompile project(':dd-java-agent:instrumentation:couchbase-2.0') + testCompile project(path: ':dd-java-agent:instrumentation:couchbase-2.0', configuration: 'testArtifacts') + testCompile group: 'com.couchbase.mock', name: 'CouchbaseMock', version: '1.5.19' + + testCompile group: 'org.springframework.data', name: 'spring-data-couchbase', version: '3.1.0.RELEASE' + + testCompile group: 'com.couchbase.client', name: 'java-client', version: '2.6.0' + testCompile group: 'com.couchbase.client', name: 'encryption', version: '1.0.0' + + latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-couchbase', version: '3.1+' + latestDepTestCompile group: 'com.couchbase.client', name: 'java-client', version: '2.6+' + latestDepTestCompile group: 'com.couchbase.client', name: 'encryption', version: '+' +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseCoreInstrumentation.java b/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseCoreInstrumentation.java new file mode 100644 index 0000000000..6b6c666968 --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseCoreInstrumentation.java @@ -0,0 +1,78 @@ +package datadog.trace.instrumentation.couchbase.client; + +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.couchbase.client.core.message.CouchbaseRequest; +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.util.GlobalTracer; +import java.util.Collections; +import java.util.Map; +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 class CouchbaseCoreInstrumentation extends Instrumenter.Default { + + public CouchbaseCoreInstrumentation() { + super("couchbase"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("com.couchbase.client.core.CouchbaseCore"); + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "com.couchbase.client.core.message.CouchbaseRequest", Span.class.getName()); + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(isPublic()) + .and(takesArgument(0, named("com.couchbase.client.core.message.CouchbaseRequest"))) + .and(named("send")), + CouchbaseCoreAdvice.class.getName()); + } + + public static class CouchbaseCoreAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addOperationIdToSpan(@Advice.Argument(0) final CouchbaseRequest request) { + + final Scope scope = GlobalTracer.get().scopeManager().active(); + if (scope != null) { + // The scope from the initial rxJava subscribe is not available to the networking layer + // To transfer the span, the span is added to the context store + + final ContextStore contextStore = + InstrumentationContext.get(CouchbaseRequest.class, Span.class); + + Span span = contextStore.get(request); + + if (span == null) { + span = GlobalTracer.get().activeSpan(); + contextStore.put(request, span); + + if (request.operationId() != null) { + span.setTag("couchbase.operation_id", request.operationId()); + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseNetworkInstrumentation.java b/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseNetworkInstrumentation.java new file mode 100644 index 0000000000..c6d0da6f7a --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/main/java/datadog/trace/instrumentation/couchbase/client/CouchbaseNetworkInstrumentation.java @@ -0,0 +1,87 @@ +package datadog.trace.instrumentation.couchbase.client; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.couchbase.client.core.message.CouchbaseRequest; +import com.couchbase.client.java.transcoder.crypto.JsonCryptoTranscoder; +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import java.util.Collections; +import java.util.Map; +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 class CouchbaseNetworkInstrumentation extends Instrumenter.Default { + public CouchbaseNetworkInstrumentation() { + super("couchbase"); + } + + @Override + public ElementMatcher typeMatcher() { + // Exact class because private fields are used + return safeHasSuperType(named("com.couchbase.client.core.endpoint.AbstractGenericHandler")); + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "com.couchbase.client.core.message.CouchbaseRequest", Span.class.getName()); + } + + @Override + public Map, String> transformers() { + // encode(ChannelHandlerContext ctx, REQUEST msg, List out) + return singletonMap( + isMethod() + .and(named("encode")) + .and(takesArguments(3)) + .and( + takesArgument( + 0, named("com.couchbase.client.deps.io.netty.channel.ChannelHandlerContext"))) + .and(takesArgument(2, named("java.util.List"))), + CouchbaseNetworkAdvice.class.getName()); + } + + public static class CouchbaseNetworkAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addNetworkTagsToSpan( + @Advice.FieldValue("remoteHostname") final String remoteHostname, + @Advice.FieldValue("remoteSocket") final String remoteSocket, + @Advice.FieldValue("localSocket") final String localSocket, + @Advice.Argument(1) final CouchbaseRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(CouchbaseRequest.class, Span.class); + + final Span span = contextStore.get(request); + if (span != null) { + Tags.PEER_HOSTNAME.set(span, remoteHostname); + + if (remoteSocket != null) { + final int splitIndex = remoteSocket.lastIndexOf(":"); + if (splitIndex != -1) { + Tags.PEER_PORT.set(span, Integer.valueOf(remoteSocket.substring(splitIndex + 1))); + } + } + + span.setTag("local.address", localSocket); + } + } + + // 2.6.0 and above + public static void muzzleCheck(final JsonCryptoTranscoder transcoder) { + transcoder.documentType(); + } + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseAsyncClient26Test.groovy b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseAsyncClient26Test.groovy new file mode 100644 index 0000000000..2170d9ac9e --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseAsyncClient26Test.groovy @@ -0,0 +1,9 @@ +import datadog.trace.agent.test.asserts.TraceAssert + +class CouchbaseAsyncClient26Test extends CouchbaseAsyncClientTest { + + @Override + void assertCouchbaseCall(TraceAssert trace, int index, String name, String bucketName = null, Object parentSpan = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan) + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseClient26Test.groovy b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseClient26Test.groovy new file mode 100644 index 0000000000..3b37a49305 --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseClient26Test.groovy @@ -0,0 +1,8 @@ +import datadog.trace.agent.test.asserts.TraceAssert + +class CouchbaseClient26Test extends CouchbaseClientTest { + @Override + void assertCouchbaseCall(TraceAssert trace, int index, String name, String bucketName = null, Object parentSpan = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan) + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseSpanUtil.groovy b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseSpanUtil.groovy new file mode 100644 index 0000000000..1c08aca5bf --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/CouchbaseSpanUtil.groovy @@ -0,0 +1,42 @@ +import datadog.opentracing.DDSpan +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.api.DDSpanTypes +import io.opentracing.tag.Tags + +class CouchbaseSpanUtil { + // Reusable span assertion method. Cannot directly override AbstractCouchbaseTest.assertCouchbaseSpan because + // Of the class hierarchy of these tests + static void assertCouchbaseCall(TraceAssert trace, int index, String name, String bucketName = null, Object parentSpan = null) { + trace.span(index) { + serviceName "couchbase" + resourceName name + operationName "couchbase.call" + spanType DDSpanTypes.COUCHBASE + errored false + if (parentSpan == null) { + parent() + } else { + childOf((DDSpan) parentSpan) + } + tags { + "$Tags.COMPONENT.key" "couchbase-client" + "$Tags.DB_TYPE.key" "couchbase" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + if (bucketName != null) { + "bucket" bucketName + } + + // Because of caching, not all requests hit the server so these tags may be absent + "$Tags.PEER_HOSTNAME.key" { it == "localhost" || it == "127.0.0.1" || it == null } + "$Tags.PEER_PORT.key" { it == null || Number } + "local.address" { it == null || String } + + // Not all couchbase operations have operation id. Notably, 'ViewQuery's do not + // We assign a resourceName of 'Bucket.query' and this is shared with n1ql queries + // that do have operation ids + "couchbase.operation_id" { it == null || String } + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy new file mode 100644 index 0000000000..bc6ce48b0c --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy @@ -0,0 +1,11 @@ +package springdata + + +import datadog.trace.agent.test.asserts.TraceAssert + +class CouchbaseSpringRepository26Test extends CouchbaseSpringRepositoryTest { + @Override + void assertCouchbaseCall(TraceAssert trace, int index, String name, String bucketName = null, Object parentSpan = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan) + } +} diff --git a/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy new file mode 100644 index 0000000000..7072f65724 --- /dev/null +++ b/dd-java-agent/instrumentation/couchbase-2.6/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy @@ -0,0 +1,10 @@ +package springdata + +import datadog.trace.agent.test.asserts.TraceAssert + +class CouchbaseSpringTemplate26Test extends CouchbaseSpringTemplateTest { + @Override + void assertCouchbaseCall(TraceAssert trace, int index, String name, String bucketName = null, Object parentSpan = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan) + } +} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java index 45404a1d9c..ade1181f9b 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -61,6 +62,7 @@ import spock.lang.Specification; @SpecMetadata(filename = "AgentTestRunner.java", line = 0) @Slf4j public abstract class AgentTestRunner extends Specification { + private static final long TIMEOUT_MILLIS = 10 * 1000; /** * For test runs, agent's global tracer will report to this list writer. * @@ -206,10 +208,15 @@ public abstract class AgentTestRunner extends Specification { @SneakyThrows public static void blockUntilChildSpansFinished(final int numberOfSpans) { final Span span = io.opentracing.util.GlobalTracer.get().activeSpan(); + final long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS; if (span instanceof DDSpan) { final PendingTrace pendingTrace = ((DDSpan) span).context().getTrace(); while (pendingTrace.size() < numberOfSpans) { + if (System.currentTimeMillis() > deadline) { + throw new TimeoutException( + "Timed out waiting for child spans. Received: " + pendingTrace.size()); + } Thread.sleep(10); } } diff --git a/dd-java-agent/testing/src/test/groovy/AgentTestRunnerTest.groovy b/dd-java-agent/testing/src/test/groovy/AgentTestRunnerTest.groovy index f518b17126..2188723830 100644 --- a/dd-java-agent/testing/src/test/groovy/AgentTestRunnerTest.groovy +++ b/dd-java-agent/testing/src/test/groovy/AgentTestRunnerTest.groovy @@ -10,6 +10,7 @@ import io.opentracing.Tracer import spock.lang.Shared import java.lang.reflect.Field +import java.util.concurrent.TimeoutException import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static datadog.trace.api.Config.TRACE_CLASSES_EXCLUDE @@ -67,6 +68,16 @@ class AgentTestRunnerTest extends AgentTestRunner { bootstrapClassesIncorrectlyLoaded == [] } + def "waiting for child spans times out"() { + when: + runUnderTrace("parent") { + blockUntilChildSpansFinished(1) + } + + then: + thrown(TimeoutException) + } + def "logging works"() { when: org.slf4j.LoggerFactory.getLogger(AgentTestRunnerTest).debug("hello") diff --git a/settings.gradle b/settings.gradle index 8dbd7246e0..50dba5176f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,7 @@ include ':dd-java-agent:instrumentation:apache-httpclient-4' include ':dd-java-agent:instrumentation:aws-java-sdk-1.11.0' include ':dd-java-agent:instrumentation:aws-java-sdk-2.2' include ':dd-java-agent:instrumentation:couchbase-2.0' +include ':dd-java-agent:instrumentation:couchbase-2.6' include ':dd-java-agent:instrumentation:datastax-cassandra-3' include ':dd-java-agent:instrumentation:dropwizard' include ':dd-java-agent:instrumentation:dropwizard:dropwizard-views'