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.
| Span span = operationContext.getTracingManager().createOperationSpan( | ||
| actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); | ||
| if (span != null) { | ||
| span.openScope(); | ||
| } | ||
| ReadBinding binding = getReadBinding(readPreference, actualClientSession, implicitSession); | ||
|
|
There was a problem hiding this comment.
span.openScope() is called before getReadBinding(...). If getReadBinding throws (e.g., cluster/binding construction failure), the scope will remain open because the finally block is never entered. Wrap scope open/close in an outer try/finally (or move openScope() inside the existing try) so closeScope() is guaranteed to run even when binding creation fails.
| Span span = operationContext.getTracingManager().createOperationSpan( | ||
| actualClientSession.getTransactionSpan(), operationContext, operation.getCommandName(), operation.getNamespace()); | ||
| if (span != null) { | ||
| span.openScope(); | ||
| } | ||
| WriteBinding binding = getWriteBinding(actualClientSession, isImplicitSession(session)); | ||
|
|
There was a problem hiding this comment.
span.openScope() is called before getWriteBinding(...). If binding creation throws, the scope is leaked because closeScope() is only invoked in the finally that runs after operation.execute(...). Restructure to ensure closeScope() executes for all paths after openScope(), including failures during binding acquisition.
| @@ -473,14 +476,16 @@ private <T> T sendAndReceiveInternal(final CommandMessage message, final Decoder | |||
| commandEventSender = new NoOpCommandEventSender(); | |||
| } | |||
| if (isTracingCommandPayloadNeeded) { | |||
| tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument); | |||
| tracingSpan.setQueryText(commandDocument); | |||
| } | |||
There was a problem hiding this comment.
tracingSpan.openScope() happens before command document hydration and before the sendCommandMessage try/catch. If any exception is thrown between openScope() and the later close sites (e.g., message.getCommandDocument(bsonOutput), event sender construction), the scope will leak. Enclose the entire post-openScope() section in a try/finally that closes the scope (and ends the span as appropriate) on all exceptional paths.
| // Register default convention. Users can override by registering their own GlobalObservationConvention | ||
| // after the MongoClient is created — the last matching convention wins. | ||
| DefaultMongodbObservationConvention defaultConvention = new DefaultMongodbObservationConvention(); | ||
| observationRegistry.observationConfig().observationConvention(defaultConvention); |
There was a problem hiding this comment.
Registering DefaultMongodbObservationConvention in the MicrometerTracer constructor mutates the user-provided ObservationRegistry and will add another convention each time a MongoClient/MicrometerTracer is created with the same registry. This can lead to unbounded growth and can unintentionally override user conventions depending on Micrometer's selection order. Consider making registration idempotent (detect existing convention), or registering the convention once at a higher level rather than per-tracer instance.
| // Populate domain fields on MongodbContext — the convention reads these to produce tags | ||
| MongodbContext mongodbContext = span.getMongodbContext(); | ||
| if (mongodbContext != null) { | ||
| mongodbContext.setCommandName(commandName); |
There was a problem hiding this comment.
This method now relies on populating MongodbContext (and a convention at observation stop time) rather than directly attaching all of the tags described in the method Javadoc above. Please update the Javadoc accordingly so it reflects the new tagging mechanism and doesn’t reference tags that are no longer set here.
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