diff --git a/source/open-telemetry/open-telemetry.md b/source/open-telemetry/open-telemetry.md index 3027900988..de3f1a811e 100644 --- a/source/open-telemetry/open-telemetry.md +++ b/source/open-telemetry/open-telemetry.md @@ -135,6 +135,33 @@ When a user commits or aborts a transaction with `commitTransaction` or `abortTr In case of `withTransaction` operation spans for operations that are executed inside the callbacks SHOULD be nested into the `withTransaction` span. +##### Cursor Iteration (`getMore`) + +If the driver does not expose the cursor to the caller, but iterates it internally to produce the return value of one +public API call (e.g., a `find` helper returning an array of all matching documents), the driver MUST NOT create +additional operation spans. Every `getMore` command span for that call MUST be nested under the call's single operation +span. + +If the driver returns the cursor to the caller and the caller drives iteration, the driver MUST create a new operation +span for each `getMore` it sends. The span MUST be created within the current span of the host application and MUST be +named according to [Operation Span Name](#operation-span-name): `getMore db.collection_name` for a cursor over a +collection, or `getMore db` when the cursor targets no specific collection (e.g., a cursor from `listCollections`), per +[db.collection.name](#dbcollectionname). The `getMore` command span MUST be nested under it, per +[Instrumenting Server Commands](#instrumenting-server-commands). + +This operation span MUST NOT be nested under the operation span of the command that created the cursor. A host +application may do unrelated work between batches, and nesting each `getMore` under the cursor-creating operation would +attribute that work to the original operation. Ordinary nesting still applies otherwise. A cursor iterated inside a +`withTransaction` callback nests into the `withTransaction` span, and a cursor iterated inside a transaction started +with the core transaction API nests into the pseudo operation `transaction` span. + +Each `getMore` operation span MUST be finished once its command completes. No span is scoped to a cursor's lifetime, so +a cursor that is never exhausted (e.g., a tailable cursor) leaves nothing unfinished. + +A `getMore` is not retryable, but a change stream may resume after one fails. A resume MUST NOT extend the failed +`getMore` operation span: drivers MUST finish that span with its error, and the `killCursors`, `aggregate`, and +`getMore` commands that re-establish the cursor MUST each be nested under new operation spans. + ##### Operation Span Name The span name SHOULD be: @@ -163,6 +190,7 @@ Spans SHOULD have the following attributes: | `db.collection.name` | `string` | The collection being accessed within the database stated in `db.namespace` | Required if available | | `db.operation.name` | `string` | The name of the driver operation being executed | Required | | `db.operation.summary` | `string` | Equivalent to span name | Required | +| `db.mongodb.cursor_id` | `int64` | If a cursor is created or used in the operation (see below) | Conditional | Not all attributes are available at the moment of span creation. Drivers need to add attributes at later stages, which requires an operation span to be available throughout the complete operation lifecycle. @@ -198,6 +226,16 @@ Examples: - `abortTransaction` → *omitted* - client `bulkWrite` → *omitted* + + +###### db.mongodb.cursor_id + +If the operation creates a cursor, or operates on a single existing cursor, the `cursor_id` attribute MUST be added to +the operation span, following the same rules as the command span attribute of the same name, including its omissions: +never a literal `0`, and never for a command that may operate on several cursors at once, such as `killCursors` (see +[db.mongodb.cursor_id](#command-cursor-id)). When the driver iterates a cursor internally, the value is the id of the +cursor the operation created; for a caller-driven `getMore`, it is the id the driver sent. + ##### Exceptions If the driver operation fails with an exception, drivers MUST record an exception to the current operation span. This @@ -246,7 +284,7 @@ Spans SHOULD have the following attributes: | `db.mongodb.server_connection_id` | `int64` | Server connection id | Required if available | | `db.mongodb.driver_connection_id` | `int64` | Local connection id | Required if available | | `db.query.text` | `string` | Database command that was sent to the server. Content should be equivalent to the `document` field of the CommandStartedEvent of the command monitoring. | Conditional | -| `db.mongodb.cursor_id` | `int64` | If a cursor is created or used in the operation | Required if available | +| `db.mongodb.cursor_id` | `int64` | If a cursor is created or used in the command (see below) | Conditional | | `db.mongodb.lsid` | `string` | Logical session id | Required if available | | `db.mongodb.txn_number` | `int64` | Transaction number | Required if available | @@ -308,9 +346,22 @@ added and truncated to the provided value (similar to the Logging specification) On the `MongoClient` level this configuration can be implemented with a `MongoClient` option, for example, `tracing.query_text_max_length`. + + ###### db.mongodb.cursor_id -If the command returns a cursor, or uses a cursor, the `cursor_id` attribute SHOULD be added. +If the command creates a cursor (e.g., `find`, `aggregate`, `listIndexes`) and the server returns a non-zero cursor id, +the `cursor_id` attribute MUST be added. + +If the command operates on a single existing cursor (e.g., `getMore`), the attribute MUST be added, holding the cursor +id the driver sent. It MUST still be added when the reply returns a cursor id of `0` to signal the cursor is now +exhausted; the id the command operated on is the one worth recording. + +If a command may operate on several cursors at once (e.g., `killCursors`, whose `cursors` field is an array), the +attribute MUST be omitted: it is a single `int64` and has no defined value for such a command. + +A cursor id of `0` means no server-side cursor remains. Drivers MUST NOT add the attribute with a value of `0`, and MUST +omit it when a cursor-creating command's reply returns `0`. ##### Exceptions @@ -357,6 +408,7 @@ The OpenTelemetry specification covers all driver operations including but not l | `dropCollection` | [tests/operation/drop_collection.yml](tests/operation/drop_collection.yml) | | `dropIndexes` | [tests/operation/drop_indexes.yml](tests/operation/drop_indexes.yml) | | `find` | [tests/operation/find.yml](tests/operation/find.yml) | +| `getMore` | [tests/operation/get_more.yml](tests/operation/get_more.yml) | | `listCollections` | [tests/operation/list_collections.yml](tests/operation/list_collections.yml) | | `listDatabases` | [tests/operation/list_databases.yml](tests/operation/list_databases.yml) | | `listIndexes` | [tests/operation/list_indexes.yml](tests/operation/list_indexes.yml) | @@ -425,6 +477,11 @@ A URI options can be added later if we realise our users need it, while the oppo ## Changelog +- 2026-08-11: Specified that each `getMore` command is nested under its own new operation span, sibling to the operation + span of the command that created the cursor, when the caller drives cursor iteration. Specified that + `db.mongodb.cursor_id` MUST be added to operation spans and to command spans that create a cursor with a non-zero id + or that operate on a single existing cursor, and MUST be omitted rather than set to `0` when no server-side cursor + remains. - 2026-07-31: Allowed the `update` test to accept `multi` and `upsert` at their default values, and added `initialData` to the operation tests that create or modify collections. - 2026-06-16: Clarified that the `db.query.text` attribute should be serialized to Relaxed Extended JSON. diff --git a/source/open-telemetry/tests/README.md b/source/open-telemetry/tests/README.md index 99bcc8d293..21baff58c8 100644 --- a/source/open-telemetry/tests/README.md +++ b/source/open-telemetry/tests/README.md @@ -58,3 +58,29 @@ expectTracingMessages: 7. Create a new `MongoClient`. 8. Perform the same database operation. 9. Assert that the emitted tracing span does not include the `db.query.text` attribute. + +*Test 3: `getMore` records the cursor id it sent, not the cursor id returned* + +1. Create a `MongoClient` with tracing enabled on the client, using the `tracing.enabled` client option. +2. Insert three documents into a test collection. +3. Create a cursor over that collection with `find` and a `batchSize` of `2`, then iterate the cursor until it is + exhausted. This sends exactly one `getMore`, and the server's reply to that `getMore` returns a cursor id of `0`. +4. Assert that both the `getMore` operation span and the `getMore` command span have a `db.mongodb.cursor_id` attribute. +5. Assert that on each of those two spans, the value is the cursor id the driver sent in the `getMore` command, and not + the `0` returned in that command's reply. + +*Test 4: `getMore` inside a `withTransaction` callback nests under the transaction span* + +This test covers the convenient transaction API. The core transaction API case is covered by the unified test +[tests/transaction/get_more.yml](transaction/get_more.yml). + +This test requires a replica set or a sharded cluster running server version 4.4 or later. + +1. Create a `MongoClient` with tracing enabled on the client, using the `tracing.enabled` client option. +2. Insert three documents into a test collection. +3. Start a session and call `withTransaction`. In the callback, create a cursor over that collection with `find` and a + `batchSize` of `2`, then iterate the cursor until it is exhausted. This sends exactly one `getMore` inside the + transaction. +4. Assert that a `transaction` span was emitted, and that both the `find` operation span and the `getMore` operation + span are nested directly under it. +5. Assert that the `getMore` operation span is a sibling of the `find` operation span, and is not nested under it. diff --git a/source/open-telemetry/tests/operation/get_more.json b/source/open-telemetry/tests/operation/get_more.json new file mode 100644 index 0000000000..aa13a0afdd --- /dev/null +++ b/source/open-telemetry/tests/operation/get_more.json @@ -0,0 +1,318 @@ +{ + "description": "operation getMore", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": {} + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-get-more" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-get-more", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + ], + "tests": [ + { + "description": "getMore is nested under its own operation span", + "operations": [ + { + "name": "createFindCursor", + "object": "collection0", + "arguments": { + "filter": {}, + "batchSize": 2 + }, + "saveResultAsEntity": "cursor0" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 1 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 2 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 3 + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "find operation-get-more.test", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + } + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "find operation-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + }, + { + "name": "getMore operation-get-more.test", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.operation.name": "getMore", + "db.operation.summary": "getMore operation-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + } + }, + "nested": [ + { + "name": "getMore", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.command.name": "getMore", + "network.transport": "tcp", + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "getMore operation-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + }, + { + "description": "cursor_id is omitted when the server returns cursor id 0", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "find operation-get-more.test", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-get-more.test", + "db.mongodb.cursor_id": { + "$$exists": false + } + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "operation-get-more", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "find operation-get-more.test", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/source/open-telemetry/tests/operation/get_more.yml b/source/open-telemetry/tests/operation/get_more.yml new file mode 100644 index 0000000000..d3ac726a5a --- /dev/null +++ b/source/open-telemetry/tests/operation/get_more.yml @@ -0,0 +1,165 @@ +description: operation getMore +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: {} + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name operation-get-more + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name test +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1 } + - { _id: 2 } + - { _id: 3 } +tests: + - description: getMore is nested under its own operation span + operations: + - name: createFindCursor + object: *collection0 + arguments: + filter: {} + batchSize: 2 + saveResultAsEntity: &cursor0 cursor0 + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 1 } + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 2 } + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 3 } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: find operation-get-more.test + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: find + db.operation.summary: find operation-get-more.test + # This find created a cursor with a non-zero id, since batchSize 2 leaves it open, + # so cursor_id is required on the operation span as well as the command span. + db.mongodb.cursor_id: { $$type: [ int, long ] } + # Negative assertion: this `nested` array lists the `find` command span and nothing + # else. A driver that nests the getMore under the cursor-creating operation produces a + # second entry here, which `ignoreExtraSpans: false` rejects, because the unified test + # format spec states that ignoreExtraSpans applies at every level of the span tree. + nested: + - name: find + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: find + network.transport: tcp + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: find operation-get-more.test + db.mongodb.cursor_id: { $$type: [ int, long ] } + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] + - name: getMore operation-get-more.test + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: getMore + db.operation.summary: getMore operation-get-more.test + # This operation operates on an existing cursor, so cursor_id is required here too. + db.mongodb.cursor_id: { $$type: [ int, long ] } + nested: + - name: getMore + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: getMore + network.transport: tcp + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: getMore operation-get-more.test + # This getMore drains the cursor, so the server's reply carries cursor id 0. + # The attribute must still be present. $$type only enforces presence and BSON + # type — it cannot express "non-zero", because the unified test format has no + # numeric comparison operator, and 0 satisfies $$type: int. The requirement that + # this holds the non-zero id the driver sent rather than the reply's 0 is covered + # by prose test 3 in ../README.md. + db.mongodb.cursor_id: { $$type: [ int, long ] } + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] + + - description: cursor_id is omitted when the server returns cursor id 0 + operations: + # No batchSize: all three documents come back in the first batch, the server replies with + # cursor id 0, and no cursor is left open. The UTF `find` operation fully iterates, so a + # driver that still issued a getMore here would also be caught by ignoreExtraSpans: false. + - name: find + object: *collection0 + arguments: + filter: {} + expectResult: + - { _id: 1 } + - { _id: 2 } + - { _id: 3 } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: find operation-get-more.test + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: find + db.operation.summary: find operation-get-more.test + # This find returned everything in the first batch, so no cursor was left open and + # cursor_id must be omitted from the operation span as well as the command span. + db.mongodb.cursor_id: { $$exists: false } + nested: + - name: find + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: find + network.transport: tcp + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: find operation-get-more.test + # Cursor id 0 means no server-side cursor remains: the attribute must be + # omitted, not emitted as 0. + db.mongodb.cursor_id: { $$exists: false } + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/source/open-telemetry/tests/transaction/get_more.json b/source/open-telemetry/tests/transaction/get_more.json new file mode 100644 index 0000000000..39d2039756 --- /dev/null +++ b/source/open-telemetry/tests/transaction/get_more.json @@ -0,0 +1,209 @@ +{ + "description": "transaction getMore spans", + "schemaVersion": "1.27", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "replicaset" + ] + }, + { + "minServerVersion": "4.1.8", + "topologies": [ + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": {} + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "transaction-get-more" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "transaction-get-more", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + ], + "tests": [ + { + "description": "getMore inside a transaction nests under the transaction span", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "createFindCursor", + "object": "collection0", + "arguments": { + "filter": {}, + "batchSize": 2, + "session": "session0" + }, + "saveResultAsEntity": "cursor0" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 1 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 2 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "transaction", + "attributes": { + "db.system.name": "mongodb" + }, + "nested": [ + { + "name": "find transaction-get-more.test", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "transaction-get-more", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find transaction-get-more.test" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "transaction-get-more", + "db.collection.name": "test", + "db.command.name": "find", + "db.query.summary": "find transaction-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + }, + { + "name": "getMore transaction-get-more.test", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "transaction-get-more", + "db.collection.name": "test", + "db.operation.name": "getMore", + "db.operation.summary": "getMore transaction-get-more.test" + }, + "nested": [ + { + "name": "getMore", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "transaction-get-more", + "db.collection.name": "test", + "db.command.name": "getMore", + "db.query.summary": "getMore transaction-get-more.test", + "db.mongodb.cursor_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + }, + { + "name": "commitTransaction admin", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.operation.name": "commitTransaction", + "db.operation.summary": "commitTransaction admin" + }, + "nested": [ + { + "name": "commitTransaction", + "attributes": { + "db.system.name": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.command.name": "commitTransaction", + "db.query.summary": "commitTransaction admin" + } + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/source/open-telemetry/tests/transaction/get_more.yml b/source/open-telemetry/tests/transaction/get_more.yml new file mode 100644 index 0000000000..99e60d1694 --- /dev/null +++ b/source/open-telemetry/tests/transaction/get_more.yml @@ -0,0 +1,117 @@ +description: transaction getMore spans +schemaVersion: '1.27' +runOnRequirements: + - minServerVersion: '4.0' + topologies: + - replicaset + - minServerVersion: '4.1.8' + topologies: + - sharded + - load-balanced +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: {} + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name transaction-get-more + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name test + - session: + id: &session0 session0 + client: *client0 +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1 } + - { _id: 2 } + - { _id: 3 } +tests: + - description: getMore inside a transaction nests under the transaction span + operations: + - name: startTransaction + object: *session0 + # Three documents with a batchSize of two sends exactly one getMore, which + # exhausts the cursor inside the transaction. + - name: createFindCursor + object: *collection0 + arguments: + filter: {} + batchSize: 2 + session: *session0 + saveResultAsEntity: &cursor0 cursor0 + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 1 } + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 2 } + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 3 } + - name: commitTransaction + object: *session0 + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: transaction + attributes: + db.system.name: mongodb + # The getMore operation span is a sibling of the find operation span + # that created the cursor, not nested under it, even though both + # belong to the transaction. + nested: + - name: find transaction-get-more.test + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: find + db.operation.summary: find transaction-get-more.test + nested: + - name: find + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: find + db.query.summary: find transaction-get-more.test + db.mongodb.cursor_id: { $$type: [ int, long ] } + - name: getMore transaction-get-more.test + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: getMore + db.operation.summary: getMore transaction-get-more.test + nested: + - name: getMore + attributes: + db.system.name: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: getMore + db.query.summary: getMore transaction-get-more.test + db.mongodb.cursor_id: { $$type: [ int, long ] } + - name: commitTransaction admin + attributes: + db.system.name: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.operation.name: commitTransaction + db.operation.summary: commitTransaction admin + nested: + - name: commitTransaction + attributes: + db.system.name: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.command.name: commitTransaction + db.query.summary: commitTransaction admin diff --git a/source/unified-test-format/unified-test-format.md b/source/unified-test-format/unified-test-format.md index 13871503fe..4073efb282 100644 --- a/source/unified-test-format/unified-test-format.md +++ b/source/unified-test-format/unified-test-format.md @@ -794,6 +794,10 @@ The structure of this object is as follows: - If `true`, additional unexpected spans are allowed. - If `false` or omitted, the test runner MUST fail if any unexpected spans are detected. + This applies at every level of the span tree, not only to the top-level `spans` array. A span nested under an + expected span, but absent from that span's `nested` array, is an unexpected span. A test can therefore assert that + a command span is *not* nested under a given operation span by omitting it from that operation's `nested` array. + - `spans`: Required array of span objects. Each span describes an expected tracing event. Span object properties: @@ -3453,6 +3457,9 @@ other specs *and* collating spec changes developed in parallel or during the sam ## Changelog +- 2026-08-11: Clarified that `ignoreExtraSpans` applies at every level of the span tree, so that a test can assert a + span is not nested under a given parent by omitting it from that parent's `nested` array. + - 2026-06-17: Remove pre-4.2 version references. - 2026-03-17: **Schema version 1.28.**