Skip to content

feat: record MongoDB collection operations - #330

Draft
evlawler wants to merge 2 commits into
masterfrom
feat/mongo-hook
Draft

evlawler wants to merge 2 commits into
masterfrom
feat/mongo-hook

Conversation

@evlawler

@evlawler evlawler commented Sep 17, 2026

Copy link
Copy Markdown

The Java agent has no MongoDB support. This ports the Node agent's mongo hook to the driver's synchronous MongoCollection API (mongodb-driver-sync 3.x, 4.x and 5.x) and, as in the Node change, records each operation as a query-style event so that Mongo lands in the same digests and diffs as SQL.

What is recorded

For every hooked MongoCollection method, two events:

  1. A function call on the driver's collection class. The parameters are named the way the Node hook names them (filter, update, doc, docs, pipeline, replacement, key, indexSpec, ...) rather than with the driver's own names. A leading ClientSession is a parameter named session and a trailing result Class is resultClass.
  2. Nested under it, a sql_query event with database_type: "mongodb" whose statement is the normalized shape, for example db.people.updateOne({"name": ?}, {"$set": {"age": ?}, "$inc": {"visits": ?}}). A thrown MongoWriteException becomes an exception on both events.

Methods: insertOne, insertMany, bulkWrite, updateOne, updateMany, replaceOne, deleteOne, deleteMany, findOneAndDelete, findOneAndReplace, findOneAndUpdate, find, aggregate, watch, distinct, countDocuments, estimatedDocumentCount, createIndex, createIndexes, dropIndex, dropIndexes, listIndexes, renameCollection, drop. That is the Node list, minus the methods the Java driver does not have (findOne, options, isCapped, indexes, indexExists, indexInformation, count).

The shape rules

MongoQueryShape implements the same rules as src/hooks/mongoQuery.ts in appmap-node, and its unit tests assert the same strings: keys and operators kept in order, leaves replaced by ?, arrays collapsed to distinct element shapes, pipelines kept in order, name arguments verbatim, db.getCollection("...") for non-identifier names, and limits on depth, cycles and array length. Formatting never throws.

Where Java differs from Node

  • The hook has no compile-time dependency on the driver. MongoDocumentConverter reaches the collection's namespace and codec registry, and converts values, by reflection: Bson filters and updates through toBsonDocument with the collection's registry (falling back to the no-argument overload on bson 4.2+), WriteModels into the shape Node's bulkWrite operations have ({"insertOne": {"document": ...}}), IndexModels into {"key": ...}, and a MongoNamespace into its full name. Any failure degrades that value to ?.
  • A document that is not a Bson or a Map (a POJO, a record, a Kotlin data class) is encoded with the collection's codec registry, the way the driver will encode it, so insertOne(new Person("grace", 35)) is db.people.insertOne({"age": ?, "name": ?}). Node renders class instances as ?.
  • Options objects (UpdateOptions, IndexOptions, ...) are opaque and render as ?. Node renders the keys of its options document.
  • Only the outermost operation on a thread is recorded, through the mongo_operation unique key. A wrapper collection that delegates to the driver's implementation is recorded once.
  • If a recording starts while an operation is in flight, the return hook sees that no call was recorded and stays silent instead of unbalancing the call stack.

Test

  • MongoQueryShapeTest (16 tests) and MongoOperationTest (4 tests, covering the session and result class overloads with a stand-in ClientSession interface) in the agent's unit suite.
  • agent/test/mongo: a Gradle fixture with JUnit tests. By default they run against mongo-java-server, an in-process MongoDB, so CI needs no service. mongo.bats asserts the exact statements, the parameter names, the nesting, the exception on a duplicate insert, POJO encoding and a non-identifier collection name. It is picked up by bin/test_run like the other suites. All six bats tests pass locally on Java 21.
  • The same tests were also run against a real MongoDB 8.0.4 (MONGODB_URI=... ../gradlew test -PmongoDriverVersion=...), with driver 4.11.5 and with driver 5.6.1. All pass with both drivers, the statements are identical to the in-process run, and the session test, which only runs against a real server, records session as a parameter and leaves it out of the statement. Pipeline updates, which the in-process server rejects, succeed there and record the same statement.
  • The fixture defaults to mongodb-driver-sync 4.11.5 and mongo-java-server 1.43.0. Later mongo-java-server releases need Java 11 or 17, and driver 5.x refuses the wire protocol version 1.43 speaks.
  • ./gradlew :agent:check passes except for three tests that fail identically on master in this environment (GitUtilTest and AppMapSerializerTest reject the global gpg.format=ssh git setting, AppMapConfigTest.loadBadDirectory runs as root).

Not covered

  • The legacy DBCollection API, the reactive streams driver, and the Atlas Search index methods.
  • The query return event is created after the driver method's return event template, so its id is one higher than the method return that follows it in the event list. Event order is correct; only the ids are not monotonic across that pair.

Companion changes

Written by Claude in a Claude Code session for Elizabeth Lawler. The commit carries Claude as author.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LntddoBsqjRDRBepBx7oLZ

Port the Node agent's mongo hook to the Java driver's synchronous
MongoCollection API (mongodb-driver-sync 3.x, 4.x and 5.x). Each hooked
operation is recorded as a function call on the driver's collection class,
with parameters named the way the Node agent names them (filter, update,
doc, pipeline, ...), and, nested under it, a sql_query event with
database_type "mongodb" whose statement is the operation in shell form
with a normalized argument shape:

  db.people.updateOne({"name": ?}, {"$set": {"age": ?}, "$inc": {"visits": ?}})

MongoQueryShape implements the same rules as src/hooks/mongoQuery.ts in
appmap-node, so both agents render the same statement for the same
operation: keys and operators kept in order, leaves replaced with `?`,
arrays collapsed to their distinct element shapes, pipelines kept in
order, name arguments verbatim, and limits on depth, cycles and array
length. Formatting never throws.

The hook has no compile-time dependency on the driver. MongoDocumentConverter
reaches the collection's namespace and codec registry by reflection, turns
Bson filters and updates into documents with the collection's registry,
renders WriteModels the way the Node driver's bulkWrite operations look,
and encodes POJO documents with the collection's codec so their fields are
the shape. Options objects are opaque and render as `?`. A leading
ClientSession and a trailing result Class are recorded as parameters and
left out of the statement. The mongo_operation unique key records only the
outermost operation on a thread, so a wrapper collection that delegates to
the driver is recorded once.

The agent/test/mongo fixture runs JUnit tests against mongo-java-server, an
in-process MongoDB, so CI needs no service. It pins driver 4.11.5 and
mongo-java-server 1.43.0: later server releases need Java 11 or 17, and
driver 5.x refuses the wire protocol version 1.43 speaks. mongo.bats
asserts the statements, parameter names, nesting and exceptions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LntddoBsqjRDRBepBx7oLZ
MONGODB_URI points the tests at an external server instead of the
in-process mongo-java-server, and -PmongoDriverVersion selects the driver.
The session overloads are exercised only against a real server, since
mongo-java-server does not support sessions.

Run against MongoDB 8.0.4 with drivers 4.11.5 and 5.6.1: all tests pass,
and the statements match the in-process run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LntddoBsqjRDRBepBx7oLZ
@evlawler
evlawler marked this pull request as ready for review September 17, 2026 16:16

Copy link
Copy Markdown
Author

Back to draft. The discussion on getappmap/appmap-node#239 concluded that recording Mongo operations as sql_query is not the right shape, and that the recording should be designed so Node and Java produce one agreed structure before more code lands. The hook, the reflection converter and the fixture here are kept as reference for that redesign, which will also decide whether these events should name the driver class, as they do now, or a synthetic collection class the way the Node agent does.


Generated by Claude Code

@evlawler
evlawler marked this pull request as draft September 17, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants