Skip to content

DRIVERS-3598 Specify and test getMore span nesting in the OpenTelemetry spec - #1973

Open
blink1073 wants to merge 18 commits into
mongodb:masterfrom
blink1073:DRIVERS-3598
Open

DRIVERS-3598 Specify and test getMore span nesting in the OpenTelemetry spec#1973
blink1073 wants to merge 18 commits into
mongodb:masterfrom
blink1073:DRIVERS-3598

Conversation

@blink1073

@blink1073 blink1073 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Please complete the following before merging:

  • Is the relevant DRIVERS ticket in the PR title?

Implementers must update their test runner. Schema version 1.29 introduces the $$gte operator. Raise your unified test format runner to 1.29 and implement $$gte, or every fixture in this PR is rejected before matching begins. In PyMongo that was one constant plus a five-line matcher.

Context

Command spans must nest under "the corresponding driver operation span", which is unambiguous only while an operation sends a single command. For a cursor the spec never said which operation span a getMore belongs to, and no fixture exercised getMore, so both readings passed the suite and db.mongodb.cursor_id was asserted absent in all 25 places it appeared.

The ambiguity was not hypothetical. PyMongo had implemented one reading, a single operation span covering a cursor's whole lifetime, and the new fixture caught it on its first run.

Changes

Nesting. A caller-driven getMore gets its own operation span, sibling to the cursor-creating operation's span. Driver-internal iteration, where one public API call drains the cursor itself, creates no additional operation spans. No span is scoped to a cursor's lifetime, so a cursor that is never exhausted leaves nothing unfinished. A change-stream resume ends the failed getMore operation span rather than extending it, and the killCursors it sends gets no operation span. A cursor iterated inside a transaction nests under the withTransaction span or the pseudo operation transaction span, depending on which API started it.

db.mongodb.cursor_id. Raised from SHOULD to MUST and added to operation spans. It holds the id the driver sent for a getMore, even when that reply returns 0. It is omitted rather than emitted as 0 for a cursor-creating command that leaves no cursor open, and omitted for commands that may operate on several cursors at once.

Tests. operation/get_more.yml covers the nesting and both cursor_id outcomes. transaction/get_more.yml covers a cursor iterated inside a core-API transaction. Both use ignoreExtraSpans: false and assert db.mongodb.cursor_id: { $$gte: 1 } where the attribute is required, because a lower bound of 1 is what pins "the id sent, not the reply's 0". Prose test 3 covers the convenient transaction API's callback, which the unified format cannot express. The prose test that asserted the sent cursor id is gone, since $$gte: 1 now expresses it.

Unified test format. ignoreExtraSpans applies at every level of the span tree, without which the negative assertion in operation/get_more.yml, that the getMore is not nested under find, is not enforceable by a conformant runner. Schema version 1.29 introduces $$gte, the lower-bound counterpart to $$lte, with valid-pass/operator-gte.yml as the runner reference test mirroring operator-lte.yml.

Two decisions worth a second opinion

Neither is stated in the design document, so please confirm rather than assume:

  1. Span name getMore <db>.<collection>, following the existing convention of naming operation spans after commands rather than public-API methods. Naming it after the driver's iteration method would vary per driver and make the test unassertable cross-driver.
  2. Cursor id 0 omitted rather than emitted as a literal 0. This matches the existing find.yml and aggregate.yml fixtures, which omit cursor_id on commands that exhaust in the first batch, and OpenTelemetry's convention of omitting unavailable attributes rather than encoding a sentinel.

Verification

Both fixtures pass against PyMongo, transaction/get_more.yml unchanged from its first run, and the prose test is implemented there. The generated JSON is byte-identical to a fresh run of this repository's generator, and both fixtures are schema-valid against their declared schema version 1.29.

AI disclosure

Drafted with Claude Code (Claude Opus 5 and Sonnet 5). Reviewed by the human author before opening.

Resolves a changelog conflict in source/open-telemetry/open-telemetry.md:
upstream's DRIVERS-3597 entry and this branch's DRIVERS-3598 entry were
both prepended to the same section. Kept both, newest first.
Comment thread source/open-telemetry/open-telemetry.md Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`withTransaction` callback nests into the `withTransaction` span.
`withTransaction` callback nests into the `withTransaction` span; a cursor iterated inside a transaction created using core transactions api nests into the pseudo operation `transaction` span.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread source/open-telemetry/tests/README.md Outdated
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 transaction nests under the transaction span*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to implement this test as a unified? If we do not use the convenient transaction api, it seems to be possible. Something like this:

operations:
  - { name: startTransaction, object: *session0 }
  - 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 }
        nested:
          - name: find transaction-get-more.test
          - name: getMore transaction-get-more.test
          - name: commitTransaction admin

If we want to test withTransaction explicitly, then we need a prose test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a unified test and clarified the intent of the prose test

blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 18, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
Review feedback asked for the transaction case as a unified test rather
than a prose test. A cursor iterated inside a transaction started with
the core transaction API is expressible in the unified format, since the
operations are flat rather than inside a callback, so add
tests/transaction/get_more.yml for it.

Prose test 4 now covers the convenient transaction API specifically, and
points at the unified test for the core API case.

Also state in the spec that a cursor iterated inside a transaction
started with the core transaction API nests into the pseudo operation
`transaction` span, alongside the existing `withTransaction` case.

@comandeo-mongo comandeo-mongo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, thank you!

@blink1073

Copy link
Copy Markdown
Member Author

@paulinevos, can you please review the changes to unified-test-format.md and approve on behalf of dbx-spec-maintainers-unified-test-format?

@nhachicha
nhachicha self-requested a review August 19, 2026 14:28

@mdb-ad mdb-ad left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (reviewed unified test changes only)

blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 20, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 21, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 21, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 21, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.

@nhachicha nhachicha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on this 🥇

One idea for a possible follow-up (not blocking): since getMore operation spans are now siblings of the cursor-creating operation's span, their only correlation is the shared db.mongodb.cursor_id attribute, which backends don't render as a relationship.
OpenTelemetry defines span links for exactly this shape: a causal association between spans that must not be parent and child. It could be layered on as an optional overlay for drivers whose tracing stack supports it, without touching anything this PR specifies. Something along the lines of:

Drivers MAY add a span link from each caller-driven getMore operation span to the span context of the operation span that created the cursor. Span links complement rather than replace the requirements above: nesting, span lifetimes, and the db.mongodb.cursor_id attribute are unaffected, and the unified tests do not assert on links (an optional prose test can).

Comment on lines +104 to +110
# 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 ] }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that a numeric comparison is still possible via $$lte (but not applicable in our use case here since $$lte will only gives us an upper bound. see https://mongodb.slack.com/archives/C72LB5RPV/p1787664154418809)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added $$get support

db.namespace: *database0Name
db.collection.name: *collection0Name
db.operation.name: getMore
db.operation.summary: getMore transaction-get-more.test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing db.mongodb.cursor_id: { $$type: [ int, long ] } for the operation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (with $$gte now).

Comment thread source/open-telemetry/tests/README.md Outdated

*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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism for enabling tracing varies across drivers, some drivers enable tracing differently (e.g., the Java driver takes an ObservationRegistry via its observability settings and has no tracing.enabled option). Suggest keeping this step mechanism-neutral.

Suggested change
1. Create a `MongoClient` with tracing enabled on the client, using the `tracing.enabled` client option.
1. Create a `MongoClient` with tracing enabled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread source/open-telemetry/tests/README.md Outdated

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto https://github.com/mongodb/specifications/pull/1973/changes#diff-ad12fc81b90f7df8677fb3b088dd1c6ba4874b8177866f33a6c7a6a67dbd4256R64

Suggested change
1. Create a `MongoClient` with tracing enabled on the client, using the `tracing.enabled` client option.
1. Create a `MongoClient` with tracing enabled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Introduces $$gte, the lower-bound counterpart to $$lte, as unified test
format schema version 1.29.

The getMore fixtures now assert db.mongodb.cursor_id: { $$gte: 1 }
instead of a bare type check. That pins the requirement directly: a
getMore records the cursor id it sent, and the reply's 0 would fail the
lower bound. $$type cannot express this, since 0 is a valid int, and
$$lte only bounds from above.

Review feedback also addressed:

- Add the missing db.mongodb.cursor_id assertions to the operation spans
  in tests/transaction/get_more.yml.
- Make step 1 of prose tests 3 and 4 mechanism-neutral. Enabling tracing
  varies by driver, so naming the tracing.enabled client option excluded
  drivers that configure it differently.
blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 26, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
blink1073 added a commit to mongodb/mongo-python-driver that referenced this pull request Aug 26, 2026
Adds tests/transaction/get_more.yml from DRIVERS-3598, which covers a
cursor iterated inside a transaction started with the core transaction
API: the getMore operation span is a sibling of the find operation span
that created the cursor, and both nest under the transaction span.

Review feedback on mongodb/specifications#1973 asked for this case as a
unified test rather than a prose test. It passes against the driver
unchanged. The fixture comes from a specification change that is not
merged yet.
The nested-span tightening changes what a conformant runner must do, so
it belongs behind a version marker rather than standing as an unversioned
prose entry. Both unified test format changes in this PR ship together,
so they share schema version 1.29 rather than taking a bump each.

Follows the precedent of the 1.9 entry, which lists several changes under
one version.
@blink1073

Copy link
Copy Markdown
Member Author

One idea for a possible follow-up

Filed as DRIVERS-3627

@nhachicha nhachicha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement introducing $$gte 🎉

A few points need addressing before this is mergeable, though 👍

Syntax:

```yaml
{ $$gte: 1 }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - schema-latest.json looks out of date. The PR adds schema-1.29.json but doesn't regenerate schema-latest.json, which must match the highest version (make update-schema-latest -C source) should cover it.
(curious if you used Claude why it didn't pick this up? it's part of the AGENTS.md 🤷 )

2 - Since $$gte is a new matching operator, could this also add a runner reference test under source/unified-test-format/tests/valid-pass/ e.g. operator-gte.yml mirroring operator-lte.yml ?

3 - Please update the PR description/DRIVERS-3598 to emphasis that the implementer must update the test runner (bump to 1.29 with new operator)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Is a no-op because this is a behavioral change the does not show up in the schema itself. It is similar to the $$lte update.

  2. Done

  3. Done

Comment thread source/open-telemetry/tests/README.md Outdated
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*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prose test 3 is now redundant since you introduced $$gte to express what $$type couldn't.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, when I went to change it in PyMongo I noticed that this isn't redundant, because it asserts recorded value equals the id sent, which $$gte can't express. Okay to re-add it?

Comment thread source/open-telemetry/tests/README.md Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requires server version 4.4 or later

but transaction/get_more.yml (same scenario, core API) uses 4.0/4.1.8 like the existing transaction fixtures. Should these match, or is there a reason the convenient-API case needs 4.4?

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4.4 came from copying the pattern in convenient.yml. Arguably new test should target 4.4+ anyway since we're in the process of dropping 4.2. Support. I'm happy to revert if you disagree.

Comment thread source/open-telemetry/open-telemetry.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

killCursors doesn't belong in this list (it's internal cleanup), and the Python reference implementation gives it no operation span (its command span parents to the current context), so as written the reference implementation violates this MUST

Suggest removing it from the sentence and stating its treatment explicitly, e.g.: "Drivers MUST NOT create an operation span for the killCursors sent during a resume."
WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

- A resume no longer requires an operation span for its killCursors. That
  command is internal cleanup rather than a public API call, and the
  reference implementation gives it none, so the previous wording made a
  conformant driver non-conformant.
- Add tests/valid-pass/operator-gte.yml as the runner reference test for
  the new operator, mirroring operator-lte.yml.
- Update the Current Schema Version header field to 1.29.0. The changelog
  and schema file were added without it.
- Drop the prose test asserting the sent cursor id. $$gte: 1 expresses it
  directly, so it was redundant; the remaining transaction prose test is
  renumbered to 3.
- Align that prose test's server requirements with
  tests/transaction/get_more.yml. The 4.4 bound was copied from
  convenient.yml and is stricter than the convenient transactions API
  needs.
…e test

Matches the existing convenient transaction API fixture rather than the
core API bounds. Stating the reason in the text so the split between the
two transaction tests is not read as an oversight.
@blink1073

Copy link
Copy Markdown
Member Author

I'm running another patch build in PyMongo with the updated changes before re-requesting review.

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.

5 participants