Fixed Mongo client double tracing bug
* Fixes
* Addressing review comments
1. Added comments in test
2. Fixed latestDepTest failures in MongoAsyncClient by adding `declaresField`
3. Made TracingCommandListener inline in MongoClientAdvice
MODIFY
instrumentation/mongo/mongo-3.1/src/main/java/io/opentelemetry/auto/instrumentation/mongo/v3_1/MongoClientInstrumentation.java
instrumentation/mongo/mongo-3.1/src/main/java/io/opentelemetry/auto/instrumentation/mongo/v3_1/MongoClientInstrumentation.java
@@ -24,10 +24,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
import com.mongodb.MongoClientOptions;
+import com.mongodb.event.CommandListener;
import io.opentelemetry.auto.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.auto.tooling.Instrumenter;
import java.lang.reflect.Modifier;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
@@ -75,7 +77,14 @@ public final class MongoClientInstrumentation extends Instrumenter.Default {
public static class MongoClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
- public static void injectTraceListener(@Advice.This final MongoClientOptions.Builder builder) {
+ public static void injectTraceListener(
+ @Advice.This final MongoClientOptions.Builder builder,
+ @Advice.FieldValue("commandListeners") final List<CommandListener> commandListeners) {
+ for (final CommandListener commandListener : commandListeners) {
+ if (commandListener instanceof TracingCommandListener) {
+ return;
+ }
+ }
builder.addCommandListener(new TracingCommandListener());
}
}

MODIFY
instrumentation/mongo/mongo-3.1/src/test/groovy/MongoClientTest.groovy
instrumentation/mongo/mongo-3.1/src/test/groovy/MongoClientTest.groovy
@@ -68,6 +68,30 @@ class MongoClientTest extends MongoBaseTest {
collectionName = "testCollection"
}
+ // Tests the fix for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/457
+ // TracingCommandListener might get added multiple times if clientOptions are built using existing clientOptions or when calling a build method twice.
+ // This test asserts that duplicate traces are not created in those cases.
+ def "test create collection with already built ClientOptions"() {
+ setup:
+ def clientOptions = client.mongoClientOptions
+ def newClientOptions = MongoClientOptions.builder(clientOptions).build()
+ MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), newClientOptions).getDatabase(dbName)
+
+ when:
+ db.createCollection(collectionName)
+
+ then:
+ assertTraces(1) {
+ trace(0, 1) {
+ mongoSpan(it, 0, "{\"create\":\"$collectionName\",\"capped\":\"?\"}")
+ }
+ }
+
+ where:
+ dbName = "test_db"
+ collectionName = "testCollection"
+ }
+
def "test create collection no description"() {
setup:
MongoDatabase db = new MongoClient("localhost", port).getDatabase(dbName)

MODIFY
instrumentation/mongo/mongo-3.7/src/main/java/io/opentelemetry/auto/instrumentation/mongo/v3_7/MongoClientInstrumentation.java
instrumentation/mongo/mongo-3.7/src/main/java/io/opentelemetry/auto/instrumentation/mongo/v3_7/MongoClientInstrumentation.java
@@ -24,10 +24,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
import com.mongodb.MongoClientSettings;
+import com.mongodb.event.CommandListener;
import io.opentelemetry.auto.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.auto.tooling.Instrumenter;
import java.lang.reflect.Modifier;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
@@ -75,9 +77,15 @@ public final class MongoClientInstrumentation extends Instrumenter.Default {
public static class MongoClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
- public static void injectTraceListener(@Advice.This final MongoClientSettings.Builder builder) {
- final TracingCommandListener listener = new TracingCommandListener();
- builder.addCommandListener(listener);
+ public static void injectTraceListener(
+ @Advice.This final MongoClientSettings.Builder builder,
+ @Advice.FieldValue("commandListeners") final List<CommandListener> commandListeners) {
+ for (final CommandListener commandListener : commandListeners) {
+ if (commandListener instanceof TracingCommandListener) {
+ return;
+ }
+ }
+ builder.addCommandListener(new TracingCommandListener());
}
}
}

MODIFY
instrumentation/mongo/mongo-3.7/src/test/groovy/MongoClientTest.groovy
instrumentation/mongo/mongo-3.7/src/test/groovy/MongoClientTest.groovy
@@ -72,6 +72,36 @@ class MongoClientTest extends MongoBaseTest {
collectionName = "testCollection"
}
+ // Tests the fix for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/457
+ // TracingCommandListener might get added multiple times if ClientSettings are built using existing ClientSettings or when calling a build method twice.
+ // This test asserts that duplicate traces are not created in those cases.
+ def "test create collection with already built ClientSettings"() {
+ setup:
+ def clientSettings = MongoClientSettings.builder()
+ .applyToClusterSettings({ builder ->
+ builder.hosts(Arrays.asList(
+ new ServerAddress("localhost", port)))
+ .description("some-description")
+ })
+ .build()
+ def newClientSettings = MongoClientSettings.builder(clientSettings).build()
+ MongoDatabase db = MongoClients.create(newClientSettings).getDatabase(dbName)
+
+ when:
+ db.createCollection(collectionName)
+
+ then:
+ assertTraces(1) {
+ trace(0, 1) {
+ mongoSpan(it, 0, "{\"create\":\"$collectionName\",\"capped\":\"?\"}")
+ }
+ }
+
+ where:
+ dbName = "test_db"
+ collectionName = "testCollection"
+ }
+
def "test create collection no description"() {
setup:
MongoDatabase db = MongoClients.create("mongodb://localhost:" + port).getDatabase(dbName)

MODIFY
instrumentation/mongo/mongo-async-3.3/mongo-async-3.3.gradle
instrumentation/mongo/mongo-async-3.3/mongo-async-3.3.gradle
@@ -37,4 +37,6 @@ dependencies {
testCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.3.0'
latestDepTestCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '+'
+
+ testCompile project(':instrumentation:mongo:mongo-3.7')
}

MODIFY
instrumentation/mongo/mongo-async-3.3/src/main/java/io/opentelemetry/auto/instrumentation/mongoasync/v3_3/MongoAsyncClientInstrumentation.java
instrumentation/mongo/mongo-async-3.3/src/main/java/io/opentelemetry/auto/instrumentation/mongoasync/v3_3/MongoAsyncClientInstrumentation.java
package io.opentelemetry.auto.instrumentation.mongoasync.v3_3;
import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.declaresField;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
@@ -24,10 +25,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
import com.mongodb.async.client.MongoClientSettings;
+import com.mongodb.event.CommandListener;
import io.opentelemetry.auto.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.auto.tooling.Instrumenter;
import java.lang.reflect.Modifier;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
@@ -54,7 +57,8 @@ public final class MongoAsyncClientInstrumentation extends Instrumenter.Default
Modifier.PUBLIC,
null,
Collections.<TypeDescription.Generic>emptyList())))
- .and(isPublic())));
+ .and(isPublic())))
+ .and(declaresField(named("commandListeners")));
}
@Override
@@ -75,7 +79,14 @@ public final class MongoAsyncClientInstrumentation extends Instrumenter.Default
public static class MongoAsyncClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
- public static void injectTraceListener(@Advice.This final MongoClientSettings.Builder builder) {
+ public static void injectTraceListener(
+ @Advice.This final MongoClientSettings.Builder builder,
+ @Advice.FieldValue("commandListeners") final List<CommandListener> commandListeners) {
+ for (final CommandListener commandListener : commandListeners) {
+ if (commandListener instanceof TracingCommandListener) {
+ return;
+ }
+ }
builder.addCommandListener(new TracingCommandListener());
}
}

MODIFY
instrumentation/mongo/mongo-async-3.3/src/test/groovy/MongoAsyncClientTest.groovy
instrumentation/mongo/mongo-async-3.3/src/test/groovy/MongoAsyncClientTest.groovy
@@ -84,6 +84,34 @@ class MongoAsyncClientTest extends MongoBaseTest {
collectionName = "testCollection"
}
+ // Tests the fix for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/457
+ // TracingCommandListener might get added multiple times if ClientSettings are built using existing ClientSettings or when calling a build method twice.
+ // This test asserts that duplicate traces are not created in those cases.
+ def "test create collection with already built ClientSettings"() {
+ setup:
+ def clientSettings = client.settings
+ def newClientSettings = MongoClientSettings.builder(clientSettings).build()
+ MongoDatabase db = MongoClients.create(newClientSettings).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)

Fixed Mongo client double tracing bug (#471)

* Fixes #457

* Addressing review comments
1. Added comments in test
2. Fixed latestDepTest failures in MongoAsyncClient by adding `declaresField`
3. Made TracingCommandListener inline in MongoClientAdvice

Co-authored-by: Nikita Salnikov-Tarnovski <gnikem@gmail.com>