Skip to content

Commit 9da2307

Browse files
committed
fix server connection instrumentation advice not working on write queries
Write queries on the MongoDB driver >= 4.0 and <= 4.7 weren't correctly instrumented. I didn't notice it before because the tests weren't exhaustive, and my test app was using driver v5.6.
1 parent 35ee37f commit 9da2307

File tree

2 files changed

+224
-7
lines changed

2 files changed

+224
-7
lines changed

dd-java-agent/instrumentation/mongo/driver-4.0/src/main/java/datadog/trace/instrumentation/mongo/DefaultServerConnection40Instrumentation.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
44
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpanWithoutScope;
5+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan;
56
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
67
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
78
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
8-
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
99

1010
import com.google.auto.service.AutoService;
1111
import com.mongodb.connection.ConnectionDescription;
@@ -45,18 +45,14 @@ public void methodAdvice(MethodTransformer transformer) {
4545
isMethod()
4646
.and(named("command"))
4747
.and(takesArgument(0, String.class))
48-
.and(takesArgument(1, named("org.bson.BsonDocument")))
49-
// there are multiple overload, so we select the first one that matches between version
50-
// 4.0 and 5.6
51-
.and(takesArguments(6).or(takesArguments(7))),
48+
.and(takesArgument(1, named("org.bson.BsonDocument"))),
5249
DefaultServerConnection40Instrumentation.class.getName() + "$CommandAdvice");
5350

5451
transformer.applyAdvice(
5552
isMethod()
5653
.and(named("commandAsync"))
5754
.and(takesArgument(0, String.class))
58-
.and(takesArgument(1, named("org.bson.BsonDocument")))
59-
.and(takesArguments(7).or(takesArguments(8))),
55+
.and(takesArgument(1, named("org.bson.BsonDocument"))),
6056
DefaultServerConnection40Instrumentation.class.getName() + "$CommandAdvice");
6157
}
6258

@@ -70,6 +66,13 @@ public static void onEnter(
7066
return;
7167
}
7268

69+
AgentSpan existingSpan = activeSpan();
70+
if (existingSpan != null
71+
&& MongoDecorator.OPERATION_NAME.equals(existingSpan.getOperationName())) {
72+
// we don't re-run the advice if the command goes through multiple overloads
73+
return;
74+
}
75+
7376
AgentSpan span = startSpan(MongoDecorator.OPERATION_NAME);
7477
// scope is going to be closed by the MongoCommandListener
7578
activateSpanWithoutScope(span);

dd-java-agent/instrumentation/mongo/driver-4.0/src/test/groovy/DefaultServerConnection40InstrumentationTest.groovy

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
2+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan
23

34
import com.mongodb.ConnectionString
45
import com.mongodb.client.MongoClient
56
import com.mongodb.client.MongoClients
67
import com.mongodb.MongoClientSettings
8+
import com.mongodb.client.MongoCollection
9+
import com.mongodb.client.MongoDatabase
710
import com.mongodb.event.CommandListener
811
import com.mongodb.event.CommandStartedEvent
12+
import com.mongodb.internal.build.MongoDriverVersion
913
import datadog.trace.api.config.TraceInstrumentationConfig
14+
import datadog.trace.core.DDSpan
1015
import org.bson.BsonDocument
16+
import org.bson.BsonString
17+
import org.bson.Document
18+
import org.spockframework.util.VersionNumber
1119
import spock.lang.Shared
1220
import static datadog.trace.api.Config.DBM_PROPAGATION_MODE_FULL
1321

@@ -43,6 +51,16 @@ abstract class DefaultServerConnection40InstrumentationTest extends MongoBaseTes
4351
commandListener.commands.clear()
4452
client = null
4553
}
54+
55+
@Shared
56+
String query = {
57+
def version = VersionNumber.parse(MongoDriverVersion.VERSION)
58+
if (version.major == 4 && version.minor < 3) {
59+
// query is returned for versions < 4.3
60+
return ',"query":{}'
61+
}
62+
return ''
63+
}.call()
4664
}
4765

4866
abstract class DefaultServerConnection40InstrumentationEnabledTest extends DefaultServerConnection40InstrumentationTest {
@@ -85,6 +103,104 @@ abstract class DefaultServerConnection40InstrumentationEnabledTest extends Defau
85103
commentStr.contains("ddh='")
86104
commentStr.contains("traceparent='")
87105
}
106+
107+
def "test insert comment injection"() {
108+
setup:
109+
String collectionName = randomCollectionName()
110+
DDSpan setupSpan = null
111+
MongoCollection<Document> collection = runUnderTrace("setup") {
112+
setupSpan = activeSpan() as DDSpan
113+
MongoDatabase db = client.getDatabase(databaseName)
114+
db.createCollection(collectionName)
115+
return db.getCollection(collectionName)
116+
}
117+
TEST_WRITER.waitUntilReported(setupSpan)
118+
TEST_WRITER.clear()
119+
120+
when:
121+
collection.insertOne(new Document("password", "SECRET"))
122+
def estimatedCount = collection.estimatedDocumentCount()
123+
TEST_WRITER.waitForTraces(2)
124+
125+
then:
126+
estimatedCount == 1
127+
assertTraces(2) {
128+
trace(1) {
129+
mongoSpan(it, 0, "insert", "{\"insert\":\"$collectionName\",\"ordered\":true,\"comment\":\"?\",\"documents\":[]}", false, "some-description", null, true)
130+
}
131+
trace(1) {
132+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query,\"comment\":\"?\"}", false, "some-description", null, true)
133+
}
134+
}
135+
}
136+
137+
def "test update comment injection"() {
138+
setup:
139+
String collectionName = randomCollectionName()
140+
DDSpan setupSpan = null
141+
MongoCollection<Document> collection = runUnderTrace("setup") {
142+
setupSpan = activeSpan() as DDSpan
143+
MongoDatabase db = client.getDatabase(databaseName)
144+
db.createCollection(collectionName)
145+
def coll = db.getCollection(collectionName)
146+
coll.insertOne(new Document("password", "OLDPW"))
147+
return coll
148+
}
149+
TEST_WRITER.waitUntilReported(setupSpan)
150+
TEST_WRITER.clear()
151+
152+
when:
153+
def result = collection.updateOne(
154+
new BsonDocument("password", new BsonString("OLDPW")),
155+
new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))))
156+
def estimatedCount = collection.estimatedDocumentCount()
157+
TEST_WRITER.waitForTraces(2)
158+
159+
then:
160+
result.modifiedCount == 1
161+
estimatedCount == 1
162+
assertTraces(2) {
163+
trace(1) {
164+
mongoSpan(it, 0, "update", "{\"update\":\"$collectionName\",\"ordered\":true,\"comment\":\"?\",\"updates\":[]}", false, "some-description", null, true)
165+
}
166+
trace(1) {
167+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query,\"comment\":\"?\"}", false, "some-description", null, true)
168+
}
169+
}
170+
}
171+
172+
def "test delete comment injection"() {
173+
setup:
174+
String collectionName = randomCollectionName()
175+
DDSpan setupSpan = null
176+
MongoCollection<Document> collection = runUnderTrace("setup") {
177+
setupSpan = activeSpan() as DDSpan
178+
MongoDatabase db = client.getDatabase(databaseName)
179+
db.createCollection(collectionName)
180+
def coll = db.getCollection(collectionName)
181+
coll.insertOne(new Document("password", "SECRET"))
182+
return coll
183+
}
184+
TEST_WRITER.waitUntilReported(setupSpan)
185+
TEST_WRITER.clear()
186+
187+
when:
188+
def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")))
189+
def estimatedCount = collection.estimatedDocumentCount()
190+
TEST_WRITER.waitForTraces(2)
191+
192+
then:
193+
result.deletedCount == 1
194+
estimatedCount == 0
195+
assertTraces(2) {
196+
trace(1) {
197+
mongoSpan(it, 0, "delete", "{\"delete\":\"$collectionName\",\"ordered\":true,\"comment\":\"?\",\"deletes\":[]}", false, "some-description", null, true)
198+
}
199+
trace(1) {
200+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query,\"comment\":\"?\"}", false, "some-description", null, true)
201+
}
202+
}
203+
}
88204
}
89205

90206
abstract class DefaultServerConnection40InstrumentationDisabledTest extends DefaultServerConnection40InstrumentationTest {
@@ -110,6 +226,104 @@ abstract class DefaultServerConnection40InstrumentationDisabledTest extends Defa
110226
createCommand != null
111227
!createCommand.containsKey("comment")
112228
}
229+
230+
def "test insert comment not injected when disabled"() {
231+
setup:
232+
String collectionName = randomCollectionName()
233+
DDSpan setupSpan = null
234+
MongoCollection<Document> collection = runUnderTrace("setup") {
235+
setupSpan = activeSpan() as DDSpan
236+
MongoDatabase db = client.getDatabase(databaseName)
237+
db.createCollection(collectionName)
238+
return db.getCollection(collectionName)
239+
}
240+
TEST_WRITER.waitUntilReported(setupSpan)
241+
TEST_WRITER.clear()
242+
243+
when:
244+
collection.insertOne(new Document("password", "SECRET"))
245+
def estimatedCount = collection.estimatedDocumentCount()
246+
TEST_WRITER.waitForTraces(2)
247+
248+
then:
249+
estimatedCount == 1
250+
assertTraces(2) {
251+
trace(1) {
252+
mongoSpan(it, 0, "insert", "{\"insert\":\"$collectionName\",\"ordered\":true,\"documents\":[]}")
253+
}
254+
trace(1) {
255+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query}")
256+
}
257+
}
258+
}
259+
260+
def "test update comment not injected when disabled"() {
261+
setup:
262+
String collectionName = randomCollectionName()
263+
DDSpan setupSpan = null
264+
MongoCollection<Document> collection = runUnderTrace("setup") {
265+
setupSpan = activeSpan() as DDSpan
266+
MongoDatabase db = client.getDatabase(databaseName)
267+
db.createCollection(collectionName)
268+
def coll = db.getCollection(collectionName)
269+
coll.insertOne(new Document("password", "OLDPW"))
270+
return coll
271+
}
272+
TEST_WRITER.waitUntilReported(setupSpan)
273+
TEST_WRITER.clear()
274+
275+
when:
276+
def result = collection.updateOne(
277+
new BsonDocument("password", new BsonString("OLDPW")),
278+
new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))))
279+
def estimatedCount = collection.estimatedDocumentCount()
280+
TEST_WRITER.waitForTraces(2)
281+
282+
then:
283+
result.modifiedCount == 1
284+
estimatedCount == 1
285+
assertTraces(2) {
286+
trace(1) {
287+
mongoSpan(it, 0, "update", "{\"update\":\"$collectionName\",\"ordered\":true,\"updates\":[]}")
288+
}
289+
trace(1) {
290+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query}")
291+
}
292+
}
293+
}
294+
295+
def "test delete comment not injected when disabled"() {
296+
setup:
297+
String collectionName = randomCollectionName()
298+
DDSpan setupSpan = null
299+
MongoCollection<Document> collection = runUnderTrace("setup") {
300+
setupSpan = activeSpan() as DDSpan
301+
MongoDatabase db = client.getDatabase(databaseName)
302+
db.createCollection(collectionName)
303+
def coll = db.getCollection(collectionName)
304+
coll.insertOne(new Document("password", "SECRET"))
305+
return coll
306+
}
307+
TEST_WRITER.waitUntilReported(setupSpan)
308+
TEST_WRITER.clear()
309+
310+
when:
311+
def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")))
312+
def estimatedCount = collection.estimatedDocumentCount()
313+
TEST_WRITER.waitForTraces(2)
314+
315+
then:
316+
result.deletedCount == 1
317+
estimatedCount == 0
318+
assertTraces(2) {
319+
trace(1) {
320+
mongoSpan(it, 0, "delete", "{\"delete\":\"$collectionName\",\"ordered\":true,\"deletes\":[]}")
321+
}
322+
trace(1) {
323+
mongoSpan(it, 0, "count", "{\"count\":\"$collectionName\"$query}")
324+
}
325+
}
326+
}
113327
}
114328

115329
// Test class with DBM propagation enabled by default

0 commit comments

Comments
 (0)