PYTHON-5947 extends the command-span support added in PYTHON-5945 (PR #2946) with:
- Operation-level spans — one span per public-API call (
find_one,insert_one,aggregate, etc.), with command spans nested as children (including one per retry attempt). - Transaction pseudo-spans — a
"transaction"span wrapping everything betweenstart_transaction()andcommit_transaction()/abort_transaction(). - Wiring the unified test format's
observeTracingMessages/expectTracingMessagesintotest/unified_format.py, and vendoring the DRIVERS-719 spec's YAML/JSON test files.
The two prose tests from PYTHON-5945 (env var enable/disable; env-var-driven db.query.text) are only partially
done: they pass today but only assert on command spans. Both are marked TODO(PYTHON-5947) to also verify the
operation span once it exists, and to disambiguate command-span vs. operation-span lookups. The YAML/JSON spec tests
still need to be added regardless.
Tested directly that OTel's default tracer.start_span(...) (no explicit context=) already parents to whatever
span is ambient/current:
with tracer.start_as_current_span("find mydb.coll") as op_span:
cmd_span = _otel.start_command_span(...) # unmodified PYTHON-5945 codeThe command span's parent matched the operation span automatically. So pymongo/_otel.py's command-span functions
and pymongo/_telemetry.py's _CommandTelemetry need no changes — PYTHON-5947 is purely additive: enter the
operation/transaction span via start_as_current_span() (or an explicit context) before any command spans start
underneath it, and nesting just works.
(start_span/end_span_success/end_span_failure were already renamed to start_command_span/
end_command_span_success/end_command_span_failure in PYTHON-5945 specifically to leave naming room for
start_operation_span/start_transaction_span — don't redo it.)
TracingOptions(thetracingclient option) — the sameenabledflag gates operation/transaction spans too._ADMIN_DBcheck in_extract_collection_name— the same collection-omission rule applies todb.operation.name/db.collection.name.- The
"{name} {dbname}.{collection}"summary format (_build_query_summary) — identical format fordb.operation.summary; consider generalizing to a shared helper. _is_sensitive_command— unaffected by the new operation-span layer.
- Hook point:
AsyncMongoClient._retryable_read/_retryable_write→_ClientConnectionRetryable.run(). This is the single retry-loop choke point for all CRUD operations, already carrying the operation name and a stable operation id. Enter the span once before the retry loop; end it whenrun()returns or the final exception is re-raised. - Gap to close: this path doesn't currently receive
dbname/collection name, only the operation name/id — thread those through the same way, or set them lazily (the spec allows attributes to be added after span creation). - New
_otel.pyfunctions:start_operation_span(...)(viastart_as_current_span, notstart_span) andend_operation_span_success/end_operation_span_failure. Consider a sixth telemetry class in_telemetry.py,_OperationTelemetry, matching the file's existing pattern. - Client bulk write — investigated:
AsyncMongoClient.bulk_write()calls_AsyncClientBulk.execute(), which for acknowledged writes goes through_retryable_write/_ClientConnectionRetryable.run()exactly once perbulk_write()call (not once per batch — the whole multi-batch loop lives inside that one retryable call). Unacknowledged (w=0) writes bypass_retryable_writeentirely. Soexecute(), notrun(), is the right hook point — it's the one place that covers both paths.db.namespace="admin", no single collection (matches spec). All server-side batches naturally nest under one operation span for free, since they all happen inside that oneexecute()call.
Highest-complexity piece of this ticket — investigated, design resolved:
- Hook point:
start_transaction/commit_transaction/abort_transactiononAsyncClientSession. There's already an internal_Transactionstate object holding transaction state as plain instance attributes (state,pinned_address,conn_mgr,recovery_token) — the natural home for a newspanfield, set instart_transactionand cleared when the transaction ends. - Plumbing already exists:
sessionis already threaded all the way down to_run_command(where command spans are created), sosession._transaction.spanis reachable with no new parameter passing. - Design decision — explicit per-session context lookup, not ambient attach/detach: considered OTel's
context.attach()/detach()to push the transaction span onto the ambient/thread-local (or asyncio-contextvar) context. Rejected: that scope is "whatever runs next in this thread/task," not "operations using this specific session" — a second, unrelated session's operation running in the same coroutine/task after the first session'sstart_transaction()would incorrectly inherit its context. Explicit lookup off the specificsessionobject has no such risk, and matches how pymongo already stores other cross-cutting session state (pinned_address,conn_mgr,recovery_token) as plain attributes rather than thread-locals. So: store the span onsession._transaction.span, and passcontext=trace.set_span_in_context(span)explicitly wherever a command/operation span is started under an active transaction. - Ending the span:
commit_transaction()/abort_transaction()are the only two methods that ever end a transaction —end_session()and the session's context-manager exit both already funnel throughabort_transaction()/commit_transaction(), so ending the span in just those two methods covers every exit path (including the "transaction never started" early-return inabort_transaction(), which needs its own cleanup).
- Translate
observeTracingMessages: {enableCommandPayload}into the client'stracing={"enabled": True, "query_text_max_length": ...}kwarg.- Mapping resolved: checked the spec's
find.ymldirectly — it asserts the full, untruncated command via exact match, soenableCommandPayload: truemust map to an effectively-unlimitedquery_text_max_length(e.g. a large constant), not a modest default that would truncate and fail the assertion.false/omitted maps to unset (defers to the environment variable).
- Mapping resolved: checked the spec's
- New
check_tracing_messagesmethod, modeled on the existingcheck_log_messages. Register one process-wideTracerProvider/InMemorySpanExporter— reuse the_shared_test_provider()helper already built intest_otel.py(extract to a shared test util now that there's a second consumer). Capture spans duringrun_operations, and reconstruct the parent/child tree from the flat exporter list (via each span's parent id) to match the spec'sspans/nestedstructure — this part is genuinely new machinery.- Correlating spans to a client: no span attribute identifies which client emitted it (unlike logs'
clientId). Works today only because every spec test file has at most one client withobserveTracingMessagesactive at a time — a known assumption, not a blocker; revisit only if the spec adds a multi-observed-client test.
- Correlating spans to a client: no span attribute identifies which client emitted it (unlike logs'
- Add an
expectTracingMessagesbranch to test execution, alongside the existingexpectLogMessages/expectEventshandling. - Vendor the spec's test YAML/JSON via
.evergreen/resync-specs.sh, and add a new spec-driven test file reusing the existingotelpytest marker/Evergreen test type from PYTHON-5945. - Once the spec files run, drop
test_span_created_for_insert_and_findandtest_query_text_included_when_configuredintest_otel.py— already markedTODO(PYTHON-5947)since they duplicate coverage the spec files will provide.
- Restore the
MongoClient/AsyncMongoClientdocstring section fortracing(removed on request), expanded to cover operation/transaction spans. - Add the changelog entry (also deferred).
| File | Change |
|---|---|
pymongo/_otel.py |
Add operation/transaction span functions; consider generalizing the summary-format helper |
pymongo/_telemetry.py |
Consider a new _OperationTelemetry class |
pymongo/asynchronous/mongo_client.py (+ mirror) |
Hook the retry loop; thread dbname/collection through |
pymongo/asynchronous/client_session.py (+ mirror) |
Hook transaction start/commit/abort; store span on _Transaction |
pymongo/asynchronous/client_bulk.py (+ mirror) |
Hook execute(); db.namespace="admin", no collection |
test/unified_format.py (+ mirror) |
observeTracingMessages/expectTracingMessages/check_tracing_messages |
test/utils_shared.py |
Extract the shared TracerProvider test helper |
.evergreen/resync-specs.sh |
Add an open-telemetry spec vendoring case |
| New vendored spec test data + test file | Mirror the pattern used for other spec suites |
test/test_otel.py (+ mirror) |
Drop the two now-redundant tests |
mongo_client.py docstring, doc/changelog.rst |
Deferred documentation from PYTHON-5945 |