Conversation
The driver used a single observation name ("mongodb") for both operation-level and command-level spans, which have different sets of low-cardinality tag keys. Prometheus requires all meters sharing a name to have identical tag key sets, causing the second observation type to be silently dropped.
Split MongodbObservation.MONGODB_OBSERVATION into MONGODB_OPERATION (name "mongodb.operation") and MONGODB_COMMAND (name "mongodb.command"), each declaring its own low-cardinality key set. Updated Tracer and TracingManager to pass the observation type through span creation.
Connection IDs, cursor IDs, session IDs, transaction numbers, and exception details were tagged as low-cardinality, causing unbounded Prometheus metric cardinality since their values change per-connection, per-cursor, or per-error. Moved CLIENT_CONNECTION_ID, SERVER_CONNECTION_ID, CURSOR_ID,TRANSACTION_NUMBER, SESSION_ID, EXCEPTION_MESSAGE, EXCEPTION_TYPE, and EXCEPTION_STACKTRACE from CommandLowCardinalityKeyNames to HighCardinalityKeyNames so they appear only in traces, not in metrics. Added tagHighCardinality(KeyValue) and tagHighCardinality(KeyValues) to the Span interface to support string-valued high-cardinality tags alongside the existing BsonDocument overload.
The query text max length configuration constant was stored in every Observation.Context and extracted back in the MicrometerSpan constructor. This value never changes between observations and is not output as any signal. Pass it directly via constructor parameter instead.
Observations were created with Micrometer's generic SenderContext, preventing users from filtering or customizing MongoDB observations by context type. This blocks the ObservationConvention pattern that Spring Boot needs for tag alignment. Introduced MongodbContext extending SenderContext<Object> with Kind.CLIENT, giving users a MongoDB-specific type to register ObservationHandler<MongodbContext> or ObservationConvention<MongodbContext> instances scoped to only MongoDB observations.
…f TracingManager Replaced all imperative tagLowCardinality/tagHighCardinality calls with a convention-based approach. TracingManager and InternalStreamConnection now populate domain fields on MongodbContext, and DefaultMongodbObservationConvention reads those fields at stop time to produce the final key-values. This decouples tag naming from span creation, enabling users to register a GlobalObservationConvention<MongodbContext> to customize tag names for their environment (e.g. Spring Boot tag alignment with their existing DefaultMongoCommandTagsProvider). Added domain fields to MongodbContext: observationType, commandName, databaseName, collectionName, serverAddress, connectionId, cursorId, transactionNumber, sessionId, queryText, responseStatusCode. Removed tagLowCardinality/tagHighCardinality from the Span interface as they are no longer used.
9b4b0ad to
bf91627
Compare
Update attribute name for OpenTelemetry
The driver called observation.start()/stop() but never openScope()/ closeScope(). Without scopes, registry.getCurrentObservation() returned null during MongoDB operations, breaking context propagation for any downstream code (Spring interceptors, user observations, MDC logging). For example, in withTransaction, a user observation created inside the callback would attach to the Spring HTTP parent instead of the MongoDB transaction span, because the transaction observation was never made "current" on the thread. Added openScope()/closeScope() to the Span interface with scope lifecycle management in MongoClusterImpl (operation spans), InternalStreamConnection (command spans), and TransactionSpan.
When a getMore command arrived, the cursor_id was being set on the parent operation span's MongodbContext even though the parent observation was already stopped. Modifying an observation after stop() is undefined behavior in Micrometer
There was a problem hiding this comment.
Pull request overview
Refactors Micrometer-based tracing to align better with OpenTelemetry/Micrometer conventions by separating operation vs command observations, moving tag production into an ObservationConvention, and introducing explicit span scope management in sync execution paths.
Changes:
- Split MongoDB observations into distinct operation- and command-level observation types to avoid tag-keyset collisions (e.g., Prometheus restrictions).
- Replace imperative tagging with a
MongodbContext+DefaultMongodbObservationConventionthat derives tags from populated domain fields. - Add explicit
openScope()/closeScope()lifecycle handling for spans in sync execution code paths and update unified test modifications accordingly.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java | Updates unified test skip rules for OpenTelemetry/Micrometer-related specs. |
| driver-sync/src/test/functional/com/mongodb/client/unified/Entities.java | Minor formatting adjustment in Micrometer observability settings setup. |
| driver-sync/src/test/functional/com/mongodb/client/observability/SpanTree.java | Updates test tag-key imports to match new low/high-cardinality key enums. |
| driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java | Opens/closes span scope around sync operation execution. |
| driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java | Opens transaction-span scope when starting a transaction (sync). |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java | Ensures transaction span scope is closed before ending in finalization paths; adds openScope() API. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java | Switches span creation to operation vs command observation types and populates MongodbContext fields for conventions. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java | Updates tracer API to accept an observation type when creating spans. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java | Replaces tag APIs with scope management + query-text setting + context access. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java | Splits key names into operation vs command low-cardinality sets and reorganizes high-cardinality keys. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbContext.java | Introduces a MongoDB-specific Micrometer SenderContext to hold domain fields used by conventions. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java | Uses MongodbContext, registers the default convention, and implements new Span API. |
| driver-core/src/main/com/mongodb/internal/observability/micrometer/DefaultMongodbObservationConvention.java | New default global convention that emits tags from MongodbContext fields (including errors). |
| driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java | Uses new Span API, populates query text/status code via MongodbContext, and opens/closes scope around command spans. |
| config/checkstyle/suppressions.xml | Moves the printStackTrace suppression to the new convention class. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Move openScope() calls after code that can throw to prevent scope leaks on early exceptions in MongoClusterImpl (binding creation) and InternalStreamConnection (command setup). Update createTracingSpan Javadoc to reflect convention-based tagging.
…ntion - Renamed MongodbContext to MongodbObservationContext and moved it along with DefaultMongodbObservationConvention to the public package com.mongodb.observability.micrometer - Added observationConvention() to MicrometerObservabilitySettings so users can provide a custom convention - Added testCustomObservationConvention to AbstractMicrometerProseTest validating that a user-provided convention fully controls tag output
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
rozza
left a comment
There was a problem hiding this comment.
Its getting there - I've reiterated the copilot blockers.
| @Override | ||
| public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) { | ||
| Observation observation = getObservation(name); | ||
| public Span nextSpan(final MongodbObservation observationType, final String name, |
There was a problem hiding this comment.
This leaks the internal MongodbObservation which means we should either have an alternative enum or we move that enum into the public API.
Can that be made public?
There was a problem hiding this comment.
Fixed by moving MongodbObservation to com.mongodb.observability.micrometer.
…ction observation type - Include maxQueryTextLength in equals/hashCode - Move MongodbObservation enum to public package - Remove unused import, use FQN in Javadoc - Add MONGODB_TRANSACTION observation type with dedicated key set - Wrap span lifecycle in outer try/finally for binding failures - Reset ENV_OBSERVABILITY_ENABLED in custom convention test - Add MongodbObservation to Scala API test exclusions
…n the nested try/finally approach
rozza
left a comment
There was a problem hiding this comment.
Looking good - a couple of nits (more belt and braces).
Main change is to handle context.getObservationType() == null.
| @Beta(Reason.CLIENT) | ||
| public class MongodbObservationContext extends SenderContext<Object> { | ||
|
|
||
| private MongodbObservation observationType; |
There was a problem hiding this comment.
| private MongodbObservation observationType; | |
| @Nullable | |
| private MongodbObservation observationType; |
| if (context.getObservationType() == MongodbObservation.MONGODB_TRANSACTION) { | ||
| return KeyValues.of(MongodbObservation.TransactionLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); | ||
| } else if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) { | ||
| return getOperationLowCardinalityKeyValues(context); | ||
| } else { | ||
| return getCommandLowCardinalityKeyValues(context); | ||
| } |
There was a problem hiding this comment.
Handle null values for observationType
| if (context.getObservationType() == MongodbObservation.MONGODB_TRANSACTION) { | |
| return KeyValues.of(MongodbObservation.TransactionLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); | |
| } else if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) { | |
| return getOperationLowCardinalityKeyValues(context); | |
| } else { | |
| return getCommandLowCardinalityKeyValues(context); | |
| } | |
| MongodbObservation observationType = context.getObservationType(); | |
| if (observationType == null) { | |
| return KeyValues.empty(); | |
| } else if (observationType == MongodbObservation.MONGODB_TRANSACTION) { | |
| return KeyValues.of(MongodbObservation.TransactionLowCardinalityKeyNames.SYSTEM.withValue("mongodb")); | |
| } else if (observationType == MongodbObservation.MONGODB_OPERATION) { | |
| return getOperationLowCardinalityKeyValues(context); | |
| } else { | |
| return getCommandLowCardinalityKeyValues(context); | |
| } |
| if (span != null) { | ||
| span.openScope(); | ||
| } | ||
| try { |
There was a problem hiding this comment.
Code review tool flagged: if openScope() throws, binding (obtained at line 427) is never released because the finally block hasn't been entered. Consider moving span.openScope() inside the try block:
| if (span != null) { | |
| span.openScope(); | |
| } | |
| try { | |
| try { | |
| if (span != null) { | |
| span.openScope(); | |
| } |
Not sure if its a realistic scenario or just belt and braces.
| actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); | ||
| WriteBinding binding = getWriteBinding(actualClientSession, isImplicitSession(session)); | ||
|
|
||
| if (span != null) { |
There was a problem hiding this comment.
| if (span != null) { | |
| try { | |
| if (span != null) { | |
| span.openScope(); | |
| } | |
| return operation.execute(binding, operationContext); |
Same scenario as above.
JAVA-6159
AI Usage Summary
Claude-Caude with Opus 4.6 1M Model
What AI did well
Where the user corrected or pushed back
CommonLowCardinalityKeyNamesrefactor; user asked for suggestion only, not implementation — had to reverttagHighCardinalityin InternalStreamConnectiongetMongodbContext()insteadopenScope()in the constructor (shared by sync+reactive); user asked why scope is public — led to discovering it should only be called from synccontextWrite,contextCapture,AtomicReferencepatterns — all failed due toMono.from(subscriber -> ...)pattern. User questionedHooks.enableAutomaticContextPropagationcontext-propagation=autoalone would suffice — AI confirmed, user decided to stash reactive changes and keep it simpleTransactionSpanconstructoroperationSpan.context()contextWriteandcontextCaptureto prove tests still passed — exposed that tests weren't actually validating the code they claimed to test