Extract Mongo library instrumentation (#2789)

* Extract mongo library instrumentation

* Finish

* Drift

* Cleanup

* build twice

* Spot
This commit is contained in:
Anuraag Agrawal 2021-04-14 13:51:21 +09:00 committed by GitHub
parent e87564ef12
commit b416ece9c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 534 additions and 240 deletions

View File

@ -121,6 +121,7 @@ public abstract class DatabaseClientTracer<CONNECTION, STATEMENT, SANITIZEDSTATE
return null;
}
@Nullable
protected String dbConnectionString(CONNECTION connection) {
return null;
}
@ -129,6 +130,7 @@ public abstract class DatabaseClientTracer<CONNECTION, STATEMENT, SANITIZEDSTATE
netPeerAttributes.setNetPeer(span, peerAddress(connection));
}
@Nullable
protected abstract InetSocketAddress peerAddress(CONNECTION connection);
protected void onStatement(

View File

@ -10,10 +10,9 @@ muzzle {
}
dependencies {
implementation(project(':instrumentation:mongo:mongo-common:javaagent'))
implementation(project(':instrumentation:mongo:mongo-3.1:library'))
library group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0'
testImplementation project(':instrumentation:mongo:mongo-testing')
testImplementation group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5'
testImplementation project(':instrumentation:mongo:mongo-3.1:testing')
}

View File

@ -17,7 +17,6 @@ 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.javaagent.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
@ -68,11 +67,11 @@ public class MongoClientInstrumentationModule extends InstrumentationModule {
@Advice.This MongoClientOptions.Builder builder,
@Advice.FieldValue("commandListeners") List<CommandListener> commandListeners) {
for (CommandListener commandListener : commandListeners) {
if (commandListener instanceof TracingCommandListener) {
if (commandListener == MongoInstrumentationSingletons.LISTENER) {
return;
}
}
builder.addCommandListener(new TracingCommandListener());
builder.addCommandListener(MongoInstrumentationSingletons.LISTENER);
}
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongo.v3_1;
import com.mongodb.event.CommandListener;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing;
public final class MongoInstrumentationSingletons {
public static final CommandListener LISTENER =
MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener();
private MongoInstrumentationSingletons() {}
}

View File

@ -3,167 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import com.mongodb.MongoClient
import com.mongodb.MongoClientOptions
import com.mongodb.MongoTimeoutException
import com.mongodb.ServerAddress
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import org.bson.BsonDocument
import org.bson.BsonString
import org.bson.Document
import spock.lang.Shared
class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
@Shared
MongoClient client
def setupSpec() throws Exception {
client = new MongoClient(new ServerAddress("localhost", port),
MongoClientOptions.builder()
.description("some-description")
.build())
}
def cleanupSpec() throws Exception {
client?.close()
client = null
}
import io.opentelemetry.instrumentation.mongo.v3_1.AbstractMongo31ClientTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
class MongoClientTest extends AbstractMongo31ClientTest implements AgentTestTrait {
@Override
void createCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
void createCollectionNoDescription(String dbName, String collectionName) {
MongoDatabase db = new MongoClient("localhost", port).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) {
def clientOptions = client.mongoClientOptions
def newClientOptions = MongoClientOptions.builder(clientOptions).build()
MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), newClientOptions).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)
return db.getCollection(collectionName).count()
}
@Override
MongoCollection<Document> setupInsert(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
return db.getCollection(collectionName)
}
ignoreTracesAndClear(1)
return collection
}
@Override
int insert(MongoCollection<Document> collection) {
collection.insertOne(new Document("password", "SECRET"))
return collection.count()
}
@Override
MongoCollection<Document> setupUpdate(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
def coll = db.getCollection(collectionName)
coll.insertOne(new Document("password", "OLDPW"))
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
int update(MongoCollection<Document> collection) {
def result = collection.updateOne(
new BsonDocument("password", new BsonString("OLDPW")),
new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))))
collection.count()
return result.modifiedCount
}
@Override
MongoCollection<Document> setupDelete(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
def coll = db.getCollection(collectionName)
coll.insertOne(new Document("password", "SECRET"))
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
int delete(MongoCollection<Document> collection) {
def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")))
collection.count()
return result.deletedCount
}
@Override
MongoCollection<Document> setupGetMore(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
def coll = db.getCollection(collectionName)
coll.insertMany([new Document("_id", 0), new Document("_id", 1), new Document("_id", 2)])
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
void getMore(MongoCollection<Document> collection) {
collection.find().filter(new Document("_id", new Document('$gte', 0)))
.batchSize(2).into(new ArrayList())
}
@Override
void error(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
return db.getCollection(collectionName)
}
ignoreTracesAndClear(1)
collection.updateOne(new BsonDocument(), new BsonDocument())
}
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 = createCollectionName()
void configureMongoClientOptions(MongoClientOptions.Builder options) {
}
}

View File

@ -0,0 +1,8 @@
apply from: "$rootDir/gradle/instrumentation-library.gradle"
apply plugin: "net.ltgt.errorprone"
dependencies {
library group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0'
testImplementation project(':instrumentation:mongo:mongo-3.1:testing')
}

View File

@ -3,13 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongo;
package io.opentelemetry.instrumentation.mongo.v3_1;
import static java.util.Arrays.asList;
import com.mongodb.ServerAddress;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.event.CommandStartedEvent;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer;
import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes;
@ -29,46 +30,49 @@ import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.bson.json.JsonWriter;
import org.bson.json.JsonWriterSettings;
import org.checkerframework.checker.nullness.qual.Nullable;
public class MongoClientTracer
final class MongoClientTracer
extends DatabaseClientTracer<CommandStartedEvent, BsonDocument, String> {
private static final MongoClientTracer TRACER = new MongoClientTracer();
private final int maxNormalizedQueryLength;
private final JsonWriterSettings jsonWriterSettings;
@Nullable private final JsonWriterSettings jsonWriterSettings;
public MongoClientTracer() {
this(32 * 1024);
}
public MongoClientTracer(int maxNormalizedQueryLength) {
super(NetPeerAttributes.INSTANCE);
MongoClientTracer(OpenTelemetry openTelemetry, int maxNormalizedQueryLength) {
super(openTelemetry, NetPeerAttributes.INSTANCE);
this.maxNormalizedQueryLength = maxNormalizedQueryLength;
this.jsonWriterSettings = createJsonWriterSettings(maxNormalizedQueryLength);
}
public static MongoClientTracer tracer() {
return TRACER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.mongo-common";
}
@Override
// TODO(anuraaga): Migrate off of StringWriter to avoid synchronization.
@SuppressWarnings("JdkObsolete")
protected String sanitizeStatement(BsonDocument command) {
StringWriter stringWriter = new StringWriter(128);
writeScrubbed(command, new JsonWriter(stringWriter, jsonWriterSettings), true);
// jsonWriterSettings is generally not null but could be due to security manager or unknown
// API incompatibilities, which we can't detect by Muzzle because we use reflection.
JsonWriter jsonWriter =
jsonWriterSettings != null
? new JsonWriter(stringWriter, jsonWriterSettings)
: new JsonWriter(stringWriter);
writeScrubbed(command, jsonWriter, true);
// If using MongoDB driver >= 3.7, the substring invocation will be a no-op due to use of
// JsonWriterSettings.Builder.maxLength in the static initializer for JSON_WRITER_SETTINGS
return stringWriter
.getBuffer()
.substring(0, Math.min(maxNormalizedQueryLength, stringWriter.getBuffer().length()));
StringBuffer buf = stringWriter.getBuffer();
if (buf.length() <= maxNormalizedQueryLength) {
return buf.toString();
}
return buf.substring(0, maxNormalizedQueryLength);
}
@Override
public String spanName(CommandStartedEvent event, BsonDocument document, String normalizedQuery) {
protected String spanName(
CommandStartedEvent event, BsonDocument document, String normalizedQuery) {
return conventionSpanName(dbName(event), event.getCommandName(), collectionName(event));
}
@ -92,6 +96,7 @@ public class MongoClientTracer
}
@Override
@Nullable
protected String dbConnectionString(CommandStartedEvent event) {
ConnectionDescription connectionDescription = event.getConnectionDescription();
if (connectionDescription != null) {
@ -109,6 +114,7 @@ public class MongoClientTracer
}
@Override
@Nullable
protected InetSocketAddress peerAddress(CommandStartedEvent event) {
if (event.getConnectionDescription() != null
&& event.getConnectionDescription().getServerAddress() != null) {
@ -130,7 +136,7 @@ public class MongoClientTracer
return event.getCommandName();
}
private static final Method IS_TRUNCATED_METHOD;
@Nullable private static final Method IS_TRUNCATED_METHOD;
static {
IS_TRUNCATED_METHOD =
@ -140,6 +146,7 @@ public class MongoClientTracer
.orElse(null);
}
@Nullable
private JsonWriterSettings createJsonWriterSettings(int maxNormalizedQueryLength) {
JsonWriterSettings settings = null;
try {
@ -175,14 +182,17 @@ public class MongoClientTracer
builderClass.getMethod("build", (Class<?>[]) null).invoke(builder, (Object[]) null);
}
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {
// Ignore
}
if (settings == null) {
try {
// Constructor removed in 4.0+ so use reflection. 4.0+ will have used the builder above.
settings = JsonWriterSettings.class.getConstructor(Boolean.TYPE).newInstance(false);
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException ignored) {
// Ignore
}
}
@ -264,6 +274,7 @@ public class MongoClientTracer
"createIndexes",
"listIndexes"));
@Nullable
private static String collectionName(CommandStartedEvent event) {
if (event.getCommandName().equals("getMore")) {
if (event.getCommand().containsKey("collection")) {

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.v3_1;
import com.mongodb.event.CommandListener;
import io.opentelemetry.api.OpenTelemetry;
/** Entrypoint to OpenTelemetry instrumentation of the MongoDB client. */
public final class MongoTracing {
/** Returns a new {@link MongoTracing} configured with the given {@link OpenTelemetry}. */
public static MongoTracing create(OpenTelemetry openTelemetry) {
return newBuilder(openTelemetry).build();
}
/** Returns a new {@link MongoTracingBuilder} configured with the given {@link OpenTelemetry}. */
public static MongoTracingBuilder newBuilder(OpenTelemetry openTelemetry) {
return new MongoTracingBuilder(openTelemetry);
}
private final MongoClientTracer tracer;
MongoTracing(OpenTelemetry openTelemetry, int maxNormalizedQueryLength) {
this.tracer = new MongoClientTracer(openTelemetry, maxNormalizedQueryLength);
}
/**
* Returns a new {@link CommandListener} that can be used with methods like {@link
* com.mongodb.MongoClientOptions.Builder#addCommandListener(CommandListener)}.
*/
public CommandListener newCommandListener() {
return new TracingCommandListener(tracer);
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.v3_1;
import io.opentelemetry.api.OpenTelemetry;
/** A builder of {@link MongoTracing}. */
public final class MongoTracingBuilder {
// Visible for testing
static final int DEFAULT_MAX_NORMALIZED_QUERY_LENGTH = 32 * 1024;
private final OpenTelemetry openTelemetry;
private int maxNormalizedQueryLength = DEFAULT_MAX_NORMALIZED_QUERY_LENGTH;
MongoTracingBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/**
* Sets the max length of recorded queries after normalization. Defaults to {@value
* DEFAULT_MAX_NORMALIZED_QUERY_LENGTH}.
*/
public MongoTracingBuilder setMaxNormalizedQueryLength(int maxNormalizedQueryLength) {
this.maxNormalizedQueryLength = maxNormalizedQueryLength;
return this;
}
/** Returns a new {@link MongoTracing} with the settings of this {@link MongoTracingBuilder}. */
public MongoTracing build() {
return new MongoTracing(openTelemetry, maxNormalizedQueryLength);
}
}

View File

@ -3,9 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongo;
import static io.opentelemetry.javaagent.instrumentation.mongo.MongoClientTracer.tracer;
package io.opentelemetry.instrumentation.mongo.v3_1;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandListener;
@ -15,13 +13,19 @@ import io.opentelemetry.context.Context;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class TracingCommandListener implements CommandListener {
final class TracingCommandListener implements CommandListener {
private final Map<Integer, Context> contextMap = new ConcurrentHashMap<>();
private final MongoClientTracer tracer;
private final Map<Integer, Context> contextMap;
TracingCommandListener(MongoClientTracer tracer) {
this.tracer = tracer;
contextMap = new ConcurrentHashMap<>();
}
@Override
public void commandStarted(CommandStartedEvent event) {
Context context = tracer().startSpan(Context.current(), event, event.getCommand());
Context context = tracer.startSpan(Context.current(), event, event.getCommand());
contextMap.put(event.getRequestId(), context);
}
@ -29,7 +33,7 @@ public class TracingCommandListener implements CommandListener {
public void commandSucceeded(CommandSucceededEvent event) {
Context context = contextMap.remove(event.getRequestId());
if (context != null) {
tracer().end(context);
tracer.end(context);
}
}
@ -37,7 +41,7 @@ public class TracingCommandListener implements CommandListener {
public void commandFailed(CommandFailedEvent event) {
Context context = contextMap.remove(event.getRequestId());
if (context != null) {
tracer().endExceptionally(context, event.getThrowable());
tracer.endExceptionally(context, event.getThrowable());
}
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.v3_1
import com.mongodb.MongoClientOptions
import io.opentelemetry.instrumentation.test.LibraryTestTrait
class MongoClientTest extends AbstractMongo31ClientTest implements LibraryTestTrait {
@Override
void configureMongoClientOptions(MongoClientOptions.Builder options) {
options.addCommandListener(MongoTracing.create(openTelemetry).newCommandListener())
}
}

View File

@ -3,10 +3,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.v3_1
import static io.opentelemetry.instrumentation.mongo.v3_1.MongoTracingBuilder.DEFAULT_MAX_NORMALIZED_QUERY_LENGTH
import static java.util.Arrays.asList
import com.mongodb.event.CommandStartedEvent
import io.opentelemetry.javaagent.instrumentation.mongo.MongoClientTracer
import io.opentelemetry.api.OpenTelemetry
import org.bson.BsonArray
import org.bson.BsonDocument
import org.bson.BsonInt32
@ -16,7 +19,7 @@ import spock.lang.Specification
class MongoClientTracerTest extends Specification {
def 'should sanitize statements to json'() {
setup:
def tracer = new MongoClientTracer()
def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH)
expect:
sanitizeStatementAcrossVersions(tracer,
@ -36,7 +39,7 @@ class MongoClientTracerTest extends Specification {
def 'should only preserve string value if it is the value of the first top-level key'() {
setup:
def tracer = new MongoClientTracer()
def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH)
expect:
sanitizeStatementAcrossVersions(tracer,
@ -48,7 +51,7 @@ class MongoClientTracerTest extends Specification {
def 'should truncate simple command'() {
setup:
def tracer = new MongoClientTracer(20)
def tracer = new MongoClientTracer(OpenTelemetry.noop(), 20)
def normalized = sanitizeStatementAcrossVersions(tracer,
new BsonDocument("cmd", new BsonString("c"))
@ -61,11 +64,11 @@ class MongoClientTracerTest extends Specification {
def 'should truncate array'() {
setup:
def tracer = new MongoClientTracer(27)
def tracer = new MongoClientTracer(OpenTelemetry.noop(), 27)
def normalized = sanitizeStatementAcrossVersions(tracer,
new BsonDocument("cmd", new BsonString("c"))
.append("f1", new BsonArray(Arrays.asList(new BsonString("c1"), new BsonString("c2"))))
.append("f1", new BsonArray(asList(new BsonString("c1"), new BsonString("c2"))))
.append("f2", new BsonString("c3")))
expect:
// this can vary because of different whitespace for different mongo versions
@ -74,7 +77,7 @@ class MongoClientTracerTest extends Specification {
def 'test span name with no dbName'() {
setup:
def tracer = new MongoClientTracer()
def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH)
def event = new CommandStartedEvent(
0, null, null, command, new BsonDocument(command, new BsonInt32(1)))

View File

@ -0,0 +1,11 @@
apply from: "$rootDir/gradle/java.gradle"
dependencies {
api project(':instrumentation:mongo:mongo-testing')
compileOnly group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0'
implementation deps.groovy
implementation deps.opentelemetryApi
implementation deps.spock
}

View File

@ -0,0 +1,184 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.v3_1
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import com.mongodb.MongoClient
import com.mongodb.MongoClientOptions
import com.mongodb.MongoTimeoutException
import com.mongodb.ServerAddress
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest
import io.opentelemetry.instrumentation.test.utils.PortUtils
import org.bson.BsonDocument
import org.bson.BsonString
import org.bson.Document
import spock.lang.Shared
abstract class AbstractMongo31ClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
abstract void configureMongoClientOptions(MongoClientOptions.Builder options);
@Shared
MongoClient client
def setupSpec() throws Exception {
def options = MongoClientOptions.builder().description("some-description")
configureMongoClientOptions(options)
client = new MongoClient(new ServerAddress("localhost", port), options.build())
}
def cleanupSpec() throws Exception {
client?.close()
client = null
}
@Override
void createCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
void createCollectionNoDescription(String dbName, String collectionName) {
def options = MongoClientOptions.builder()
configureMongoClientOptions(options)
MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), options.build()).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) {
def clientOptions = client.mongoClientOptions
def newClientOptions = MongoClientOptions.builder(clientOptions).build()
MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), newClientOptions).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
void createCollectionCallingBuildTwice(String dbName, String collectionName) {
def options = MongoClientOptions.builder().description("some-description")
configureMongoClientOptions(options)
options.build()
MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), options.build()).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)
return db.getCollection(collectionName).count()
}
@Override
MongoCollection<Document> setupInsert(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
return db.getCollection(collectionName)
}
ignoreTracesAndClear(1)
return collection
}
@Override
int insert(MongoCollection<Document> collection) {
collection.insertOne(new Document("password", "SECRET"))
return collection.count()
}
@Override
MongoCollection<Document> setupUpdate(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
def coll = db.getCollection(collectionName)
coll.insertOne(new Document("password", "OLDPW"))
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
int update(MongoCollection<Document> collection) {
def result = collection.updateOne(
new BsonDocument("password", new BsonString("OLDPW")),
new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))))
collection.count()
return result.modifiedCount
}
@Override
MongoCollection<Document> setupDelete(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
def coll = db.getCollection(collectionName)
coll.insertOne(new Document("password", "SECRET"))
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
int delete(MongoCollection<Document> collection) {
def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")))
collection.count()
return result.deletedCount
}
@Override
MongoCollection<Document> setupGetMore(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
def coll = db.getCollection(collectionName)
coll.insertMany([new Document("_id", 0), new Document("_id", 1), new Document("_id", 2)])
return coll
}
ignoreTracesAndClear(1)
return collection
}
@Override
void getMore(MongoCollection<Document> collection) {
collection.find().filter(new Document("_id", new Document('$gte', 0)))
.batchSize(2).into(new ArrayList())
}
@Override
void error(String dbName, String collectionName) {
MongoCollection<Document> collection = runUnderTrace("setup") {
MongoDatabase db = client.getDatabase(dbName)
db.createCollection(collectionName)
return db.getCollection(collectionName)
}
ignoreTracesAndClear(1)
collection.updateOne(new BsonDocument(), new BsonDocument())
}
def "test client failure"() {
setup:
def options = MongoClientOptions.builder().serverSelectionTimeout(10).build()
def client = new MongoClient(new ServerAddress("localhost", PortUtils.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 = createCollectionName()
}
}

View File

@ -19,11 +19,10 @@ muzzle {
}
dependencies {
implementation(project(':instrumentation:mongo:mongo-common:javaagent'))
implementation(project(':instrumentation:mongo:mongo-3.1:library'))
// a couple of test attribute verifications don't pass until 3.8.0
library group: 'org.mongodb', name: 'mongo-java-driver', version: '3.8.0'
testImplementation project(':instrumentation:mongo:mongo-testing')
testImplementation group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5'
}

View File

@ -19,7 +19,6 @@ import com.mongodb.MongoClientSettings;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.event.CommandListener;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
@ -74,11 +73,11 @@ public class MongoClientInstrumentationModule extends InstrumentationModule {
@Advice.This MongoClientSettings.Builder builder,
@Advice.FieldValue("commandListeners") List<CommandListener> commandListeners) {
for (CommandListener commandListener : commandListeners) {
if (commandListener instanceof TracingCommandListener) {
if (commandListener == MongoInstrumentationSingletons.LISTENER) {
return;
}
}
builder.addCommandListener(new TracingCommandListener());
builder.addCommandListener(MongoInstrumentationSingletons.LISTENER);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongo.v3_7;
import com.mongodb.event.CommandListener;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing;
public final class MongoInstrumentationSingletons {
public static final CommandListener LISTENER =
MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener();
private MongoInstrumentationSingletons() {}
}

View File

@ -13,12 +13,14 @@ import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
import org.bson.BsonDocument
import org.bson.BsonString
import org.bson.Document
import spock.lang.Shared
class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>> implements AgentTestTrait {
@Shared
MongoClient client
@ -64,6 +66,19 @@ class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>>
db.createCollection(collectionName)
}
@Override
void createCollectionCallingBuildTwice(String dbName, String collectionName) {
def clientSettings = MongoClientSettings.builder()
.applyToClusterSettings({ builder ->
builder.hosts(Arrays.asList(
new ServerAddress("localhost", port)))
.description("some-description")
})
clientSettings.build()
MongoDatabase db = MongoClients.create(clientSettings.build()).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)

View File

@ -10,7 +10,7 @@ muzzle {
}
dependencies {
implementation(project(':instrumentation:mongo:mongo-common:javaagent'))
implementation(project(':instrumentation:mongo:mongo-3.1:library'))
library group: 'org.mongodb', name: 'mongodb-driver-core', version: '4.0.0'

View File

@ -19,7 +19,6 @@ import com.mongodb.MongoClientSettings;
import com.mongodb.event.CommandListener;
import com.mongodb.internal.async.SingleResultCallback;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
@ -74,11 +73,11 @@ public class MongoClientInstrumentationModule extends InstrumentationModule {
@Advice.This MongoClientSettings.Builder builder,
@Advice.FieldValue("commandListeners") List<CommandListener> commandListeners) {
for (CommandListener commandListener : commandListeners) {
if (commandListener instanceof TracingCommandListener) {
if (commandListener == MongoInstrumentationSingletons.LISTENER) {
return;
}
}
builder.addCommandListener(new TracingCommandListener());
builder.addCommandListener(MongoInstrumentationSingletons.LISTENER);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongo.v4_0;
import com.mongodb.event.CommandListener;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing;
public final class MongoInstrumentationSingletons {
public static final CommandListener LISTENER =
MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener();
private MongoInstrumentationSingletons() {}
}

View File

@ -5,12 +5,16 @@
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import com.mongodb.MongoClientSettings
import com.mongodb.ServerAddress
import com.mongodb.client.result.DeleteResult
import com.mongodb.client.result.UpdateResult
import com.mongodb.reactivestreams.client.MongoClient
import com.mongodb.reactivestreams.client.MongoClients
import com.mongodb.reactivestreams.client.MongoCollection
import com.mongodb.reactivestreams.client.MongoDatabase
import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CountDownLatch
import org.bson.BsonDocument
@ -21,7 +25,7 @@ import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import spock.lang.Shared
class Mongo4ReactiveClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
class Mongo4ReactiveClientTest extends AbstractMongoClientTest<MongoCollection<Document>> implements AgentTestTrait {
@Shared
MongoClient client
@ -52,6 +56,18 @@ class Mongo4ReactiveClientTest extends AbstractMongoClientTest<MongoCollection<D
throw new AssumptionViolatedException("not tested on 4.0")
}
@Override
void createCollectionCallingBuildTwice(String dbName, String collectionName) {
def settings = MongoClientSettings.builder()
.applyToClusterSettings({ builder ->
builder.hosts(Arrays.asList(
new ServerAddress("localhost", port)))
})
settings.build()
MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName)
db.createCollection(collectionName).subscribe(toSubscriber {})
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)

View File

@ -5,17 +5,21 @@
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import com.mongodb.MongoClientSettings
import com.mongodb.ServerAddress
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
import org.bson.BsonDocument
import org.bson.BsonString
import org.bson.Document
import org.junit.AssumptionViolatedException
import spock.lang.Shared
class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>> implements AgentTestTrait {
@Shared
MongoClient client
@ -46,6 +50,18 @@ class MongoClientTest extends AbstractMongoClientTest<MongoCollection<Document>>
throw new AssumptionViolatedException("not tested on 4.0")
}
@Override
void createCollectionCallingBuildTwice(String dbName, String collectionName) {
def settings = MongoClientSettings.builder()
.applyToClusterSettings({ builder ->
builder.hosts(Arrays.asList(
new ServerAddress("localhost", port)))
})
settings.build()
MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName)
db.createCollection(collectionName)
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)

View File

@ -11,12 +11,11 @@ muzzle {
}
dependencies {
implementation(project(':instrumentation:mongo:mongo-common:javaagent'))
implementation(project(':instrumentation:mongo:mongo-3.1:library'))
library group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.3.0'
testImplementation project(':instrumentation:mongo:mongo-testing')
testImplementation group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '1.50.5'
testInstrumentation project(':instrumentation:mongo:mongo-3.7:javaagent')
}

View File

@ -20,7 +20,6 @@ import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoClientSettings;
import com.mongodb.event.CommandListener;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.mongo.TracingCommandListener;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
@ -76,11 +75,11 @@ public class MongoAsyncClientInstrumentationModule extends InstrumentationModule
@Advice.This MongoClientSettings.Builder builder,
@Advice.FieldValue("commandListeners") List<CommandListener> commandListeners) {
for (CommandListener commandListener : commandListeners) {
if (commandListener instanceof TracingCommandListener) {
if (commandListener == MongoInstrumentationSingletons.LISTENER) {
return;
}
}
builder.addCommandListener(new TracingCommandListener());
builder.addCommandListener(MongoInstrumentationSingletons.LISTENER);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3;
import com.mongodb.event.CommandListener;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing;
public final class MongoInstrumentationSingletons {
public static final CommandListener LISTENER =
MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener();
private MongoInstrumentationSingletons() {}
}

View File

@ -15,6 +15,8 @@ import com.mongodb.async.client.MongoDatabase
import com.mongodb.client.result.DeleteResult
import com.mongodb.client.result.UpdateResult
import com.mongodb.connection.ClusterSettings
import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CountDownLatch
import org.bson.BsonDocument
@ -23,7 +25,7 @@ import org.bson.Document
import org.junit.AssumptionViolatedException
import spock.lang.Shared
class MongoAsyncClientTest extends AbstractMongoClientTest<MongoCollection<Document>> {
class MongoAsyncClientTest extends AbstractMongoClientTest<MongoCollection<Document>> implements AgentTestTrait{
@Shared
MongoClient client
@ -64,6 +66,19 @@ class MongoAsyncClientTest extends AbstractMongoClientTest<MongoCollection<Docum
db.createCollection(collectionName, toCallback {})
}
@Override
void createCollectionCallingBuildTwice(String dbName, String collectionName) {
def settings = MongoClientSettings.builder()
.clusterSettings(
ClusterSettings.builder()
.description("some-description")
.applyConnectionString(new ConnectionString("mongodb://localhost:$port"))
.build())
settings.build()
MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName)
db.createCollection(collectionName, toCallback {})
}
@Override
int getCollection(String dbName, String collectionName) {
MongoDatabase db = client.getDatabase(dbName)

View File

@ -1,19 +0,0 @@
// not applying $rootDir/gradle/instrumentation.gradle because that brings running tests with agent
// infrastructure, and this module only wants to run unit tests
ext.mavenGroupId = 'io.opentelemetry.javaagent.instrumentation'
apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/publish.gradle"
archivesBaseName = projectDir.parentFile.name
dependencies {
compileOnly project(':instrumentation-api')
compileOnly project(':javaagent-api')
compileOnly group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0'
testImplementation project(':instrumentation-api')
testImplementation project(':javaagent-api')
testImplementation group: 'org.mongodb', name: 'mongo-java-driver', version: '3.1.0'
}

View File

@ -3,11 +3,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.mongo.testing
import static io.opentelemetry.api.trace.SpanKind.CLIENT
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.sdk.trace.data.SpanData
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
@ -17,7 +19,7 @@ import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.output.Slf4jLogConsumer
import spock.lang.Shared
abstract class AbstractMongoClientTest<T> extends AgentInstrumentationSpecification {
abstract class AbstractMongoClientTest<T> extends InstrumentationSpecification {
@Shared
GenericContainer mongodb
@ -50,6 +52,8 @@ abstract class AbstractMongoClientTest<T> extends AgentInstrumentationSpecificat
// This test asserts that duplicate traces are not created in those cases.
abstract void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName)
abstract void createCollectionCallingBuildTwice(String dbName, String collectionName)
abstract int getCollection(String dbName, String collectionName)
abstract T setupInsert(String dbName, String collectionName)
@ -120,6 +124,29 @@ abstract class AbstractMongoClientTest<T> extends AgentInstrumentationSpecificat
collectionName = createCollectionName()
}
def "test create collection calling build twice"() {
when:
runUnderTrace("parent") {
createCollectionCallingBuildTwice(dbName, collectionName)
}
then:
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent")
mongoSpan(it, 1, "create", collectionName, dbName, span(0)) {
assert it == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" ||
it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}"
true
}
}
}
where:
dbName = "test_db"
collectionName = createCollectionName()
}
def "test get collection"() {
when:
def count = runUnderTrace("parent") {

View File

@ -186,10 +186,11 @@ include ':instrumentation:logback:logback-1.0:library'
include ':instrumentation:logback:logback-1.0:testing'
include ':instrumentation:methods:javaagent'
include ':instrumentation:mongo:mongo-3.1:javaagent'
include ':instrumentation:mongo:mongo-3.1:library'
include ':instrumentation:mongo:mongo-3.1:testing'
include ':instrumentation:mongo:mongo-3.7:javaagent'
include ':instrumentation:mongo:mongo-4.0:javaagent'
include ':instrumentation:mongo:mongo-async-3.3:javaagent'
include ':instrumentation:mongo:mongo-common:javaagent'
include ':instrumentation:mongo:mongo-testing'
include ':instrumentation:netty:netty-3.8:javaagent'
include ':instrumentation:netty:netty-4.0:javaagent'