diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
index f3e6d3ef2ff..b2552959cdf 100644
--- a/config/checkstyle/suppressions.xml
+++ b/config/checkstyle/suppressions.xml
@@ -60,7 +60,7 @@
-
+
diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java
index 8f462df193e..a29aa8afc41 100644
--- a/driver-core/src/main/com/mongodb/MongoClientSettings.java
+++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java
@@ -17,6 +17,7 @@
package com.mongodb;
import com.mongodb.annotations.Alpha;
+import com.mongodb.annotations.Beta;
import com.mongodb.annotations.Immutable;
import com.mongodb.annotations.NotThreadSafe;
import com.mongodb.annotations.Reason;
@@ -517,7 +518,7 @@ public Builder transportSettings(final TransportSettings transportSettings) {
* @see #getObservabilitySettings()
* @since 5.7
*/
- @Alpha(Reason.CLIENT)
+ @Beta(Reason.CLIENT)
public Builder observabilitySettings(final ObservabilitySettings observabilitySettings) {
this.observabilitySettings = notNull("observabilitySettings", observabilitySettings);
return this;
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
index 7e454debedd..52114d84bb8 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -49,6 +49,7 @@
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.logging.StructuredLogger;
+import com.mongodb.observability.micrometer.MongodbObservationContext;
import com.mongodb.internal.observability.micrometer.Span;
import com.mongodb.internal.session.SessionContext;
import com.mongodb.internal.time.Timeout;
@@ -94,8 +95,7 @@
import static com.mongodb.internal.connection.ProtocolHelper.getSnapshotTimestamp;
import static com.mongodb.internal.connection.ProtocolHelper.isCommandOk;
import static com.mongodb.internal.logging.LogMessage.Level.DEBUG;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.RESPONSE_STATUS_CODE;
+
import static com.mongodb.internal.thread.InterruptionUtil.translateInterruptedException;
import static java.util.Arrays.asList;
@@ -454,7 +454,6 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder
() -> getDescription().getServerAddress(),
() -> getDescription().getConnectionId()
);
-
boolean isLoggingCommandNeeded = isLoggingCommandNeeded();
boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled();
@@ -473,7 +472,10 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder
commandEventSender = new NoOpCommandEventSender();
}
if (isTracingCommandPayloadNeeded) {
- tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument);
+ tracingSpan.setQueryText(commandDocument);
+ }
+ if (tracingSpan != null) {
+ tracingSpan.openScope();
}
try {
@@ -481,6 +483,8 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder
} catch (Exception e) {
if (tracingSpan != null) {
tracingSpan.error(e);
+ tracingSpan.closeScope();
+ tracingSpan.end();
}
commandEventSender.sendFailedEvent(e);
throw e;
@@ -492,6 +496,7 @@ private T sendAndReceiveInternal(final CommandMessage message, final Decoder
} else {
commandEventSender.sendSucceededEventForOneWayCommand();
if (tracingSpan != null) {
+ tracingSpan.closeScope();
tracingSpan.end();
}
return null;
@@ -585,13 +590,17 @@ private T receiveCommandMessageResponse(final Decoder decoder, final Comm
}
if (tracingSpan != null) {
if (e instanceof MongoCommandException) {
- tracingSpan.tagLowCardinality(RESPONSE_STATUS_CODE.withValue(String.valueOf(((MongoCommandException) e).getErrorCode())));
+ MongodbObservationContext ctx = tracingSpan.getMongodbObservationContext();
+ if (ctx != null) {
+ ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) e).getErrorCode()));
+ }
}
tracingSpan.error(e);
}
throw e;
} finally {
if (tracingSpan != null) {
+ tracingSpan.closeScope();
tracingSpan.end();
}
}
@@ -639,7 +648,7 @@ private void sendAndReceiveAsyncInternal(final CommandMessage message, final
commandEventSender = new NoOpCommandEventSender();
}
if (isTracingCommandPayloadNeeded) {
- tracingSpan.tagHighCardinality(QUERY_TEXT.asString(), commandDocument);
+ tracingSpan.setQueryText(commandDocument);
}
final Span commandSpan = tracingSpan;
@@ -647,8 +656,10 @@ private void sendAndReceiveAsyncInternal(final CommandMessage message, final
try {
if (t != null) {
if (t instanceof MongoCommandException) {
- commandSpan.tagLowCardinality(
- RESPONSE_STATUS_CODE.withValue(String.valueOf(((MongoCommandException) t).getErrorCode())));
+ MongodbObservationContext ctx = commandSpan.getMongodbObservationContext();
+ if (ctx != null) {
+ ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) t).getErrorCode()));
+ }
}
commandSpan.error(t);
}
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java
index a7204a01a71..1202af6ed63 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MicrometerTracer.java
@@ -18,25 +18,19 @@
import com.mongodb.MongoNamespace;
import com.mongodb.lang.Nullable;
-import io.micrometer.common.KeyValue;
-import io.micrometer.common.KeyValues;
+import com.mongodb.observability.micrometer.DefaultMongodbObservationConvention;
+import com.mongodb.observability.micrometer.MongodbObservationContext;
import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
-import io.micrometer.observation.transport.Kind;
-import io.micrometer.observation.transport.SenderContext;
import org.bson.BsonDocument;
import org.bson.BsonReader;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriter;
import org.bson.json.JsonWriterSettings;
-import java.io.PrintWriter;
import java.io.StringWriter;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_MESSAGE;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_STACKTRACE;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.EXCEPTION_TYPE;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OBSERVATION;
import static com.mongodb.internal.observability.micrometer.TracingManager.ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH;
import static java.lang.System.getenv;
import static java.util.Optional.ofNullable;
@@ -55,34 +49,30 @@ public class MicrometerTracer implements Tracer {
private final ObservationRegistry observationRegistry;
private final boolean allowCommandPayload;
private final int textMaxLength;
- private static final String QUERY_TEXT_LENGTH_CONTEXT_KEY = "QUERY_TEXT_MAX_LENGTH";
+ private final ObservationConvention convention;
/**
* Constructs a new {@link MicrometerTracer} instance.
*
* @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to.
- */
- public MicrometerTracer(final ObservationRegistry observationRegistry) {
- this(observationRegistry, false, 0);
- }
-
- /**
- * Constructs a new {@link MicrometerTracer} instance with an option to allow command payloads.
- *
- * @param observationRegistry The Micrometer {@link ObservationRegistry} to delegate tracing operations to.
* @param allowCommandPayload Whether to allow command payloads in the trace context.
+ * @param textMaxLength The maximum length for query text truncation.
+ * @param customConvention A custom observation convention, or null to use the default.
*/
- public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload, final int textMaxLength) {
+ public MicrometerTracer(final ObservationRegistry observationRegistry, final boolean allowCommandPayload,
+ final int textMaxLength, @Nullable final ObservationConvention customConvention) {
this.allowCommandPayload = allowCommandPayload;
this.observationRegistry = observationRegistry;
this.textMaxLength = ofNullable(getenv(ENV_OBSERVABILITY_QUERY_TEXT_MAX_LENGTH))
.map(Integer::parseInt)
.orElse(textMaxLength);
+ this.convention = customConvention != null ? customConvention : new DefaultMongodbObservationConvention();
}
@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,
+ @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) {
+ Observation observation = getObservation(observationType, name);
if (parent instanceof MicrometerTraceContext) {
Observation parentObservation = ((MicrometerTraceContext) parent).observation;
@@ -91,7 +81,7 @@ public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nu
}
}
- return new MicrometerSpan(observation.start(), namespace);
+ return new MicrometerSpan(observation.start(), namespace, textMaxLength);
}
@Override
@@ -104,12 +94,12 @@ public boolean includeCommandPayload() {
return allowCommandPayload;
}
- private Observation getObservation(final String name) {
- Observation observation = MONGODB_OBSERVATION.observation(observationRegistry,
- () -> new SenderContext<>((carrier, key, value) -> {}, Kind.CLIENT))
- .contextualName(name);
- observation.getContext().put(QUERY_TEXT_LENGTH_CONTEXT_KEY, textMaxLength);
- return observation;
+ private Observation getObservation(final MongodbObservation observationType, final String name) {
+ return observationType.observation(observationRegistry, () -> {
+ MongodbObservationContext ctx = new MongodbObservationContext();
+ ctx.setObservationType(observationType);
+ return ctx;
+ }).observationConvention(convention).contextualName(name);
}
/**
* Represents a Micrometer-based trace context.
@@ -135,38 +125,43 @@ private static class MicrometerSpan implements Span {
@Nullable
private final MongoNamespace namespace;
private final int queryTextLength;
+ @Nullable
+ private Observation.Scope scope;
/**
* Constructs a new {@link MicrometerSpan} instance with an associated Observation and MongoDB namespace.
*
- * @param observation The Micrometer {@link Observation}, or null if none exists.
- * @param namespace The MongoDB namespace associated with the span.
+ * @param observation The Micrometer {@link Observation}, or null if none exists.
+ * @param namespace The MongoDB namespace associated with the span.
+ * @param queryTextLength The maximum length for query text truncation.
*/
- MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace) {
+ MicrometerSpan(final Observation observation, @Nullable final MongoNamespace namespace, final int queryTextLength) {
this.namespace = namespace;
this.observation = observation;
- this.queryTextLength = ofNullable(observation.getContext().get(QUERY_TEXT_LENGTH_CONTEXT_KEY))
- .filter(Integer.class::isInstance)
- .map(Integer.class::cast)
- .orElse(Integer.MAX_VALUE);
+ this.queryTextLength = queryTextLength;
}
@Override
- public void tagLowCardinality(final KeyValue keyValue) {
- observation.lowCardinalityKeyValue(keyValue);
+ public void openScope() {
+ this.scope = observation.openScope();
}
@Override
- public void tagLowCardinality(final KeyValues keyValues) {
- observation.lowCardinalityKeyValues(keyValues);
+ public void closeScope() {
+ if (scope != null) {
+ scope.close();
+ scope = null;
+ }
}
@Override
- public void tagHighCardinality(final String keyName, final BsonDocument value) {
- observation.highCardinalityKeyValue(keyName,
- (queryTextLength < Integer.MAX_VALUE) // truncate values that are too long
- ? getTruncatedBsonDocument(value)
- : value.toString());
+ public void setQueryText(final BsonDocument commandDocument) {
+ MongodbObservationContext ctx = getMongodbObservationContext();
+ if (ctx != null) {
+ ctx.setQueryText((queryTextLength < Integer.MAX_VALUE)
+ ? getTruncatedBsonDocument(commandDocument)
+ : commandDocument.toString());
+ }
}
@Override
@@ -176,11 +171,6 @@ public void event(final String event) {
@Override
public void error(final Throwable throwable) {
- observation.lowCardinalityKeyValues(KeyValues.of(
- EXCEPTION_MESSAGE.withValue(throwable.getMessage()),
- EXCEPTION_TYPE.withValue(throwable.getClass().getName()),
- EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(throwable))
- ));
observation.error(throwable);
}
@@ -196,15 +186,17 @@ public TraceContext context() {
@Override
@Nullable
- public MongoNamespace getNamespace() {
- return namespace;
+ public MongodbObservationContext getMongodbObservationContext() {
+ if (observation.getContext() instanceof MongodbObservationContext) {
+ return (MongodbObservationContext) observation.getContext();
+ }
+ return null;
}
- private String getStackTraceAsString(final Throwable throwable) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- throwable.printStackTrace(pw);
- return sw.toString();
+ @Override
+ @Nullable
+ public MongoNamespace getNamespace() {
+ return namespace;
}
private String getTruncatedBsonDocument(final BsonDocument commandDocument) {
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java
index 0fbfe165f50..0824d28c37f 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/MongodbObservation.java
@@ -17,43 +17,66 @@
package com.mongodb.internal.observability.micrometer;
import io.micrometer.common.docs.KeyName;
-import io.micrometer.observation.Observation;
import io.micrometer.observation.docs.ObservationDocumentation;
/**
- * A MongoDB-based {@link Observation}.
+ * MongoDB {@link ObservationDocumentation} definitions for operation-level and command-level observations.
+ *
+ * These are split into two separate observation types so that each has a distinct name and a fixed set
+ * of low-cardinality tag keys. This is required by Prometheus which rejects meters that share a name
+ * but have different tag key sets.
+ *
*
* @since 5.7
*/
public enum MongodbObservation implements ObservationDocumentation {
- MONGODB_OBSERVATION {
+ /**
+ * Observation for high-level MongoDB operations (e.g. find, insert, update).
+ * Created per user-initiated operation, may contain multiple command spans.
+ */
+ MONGODB_OPERATION {
+ @Override
+ public String getName() {
+ return "mongodb.operation";
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return OperationLowCardinalityKeyNames.values();
+ }
+ },
+
+ /**
+ * Observation for wire-protocol MongoDB commands sent to the server.
+ * Created per actual command (nested under an operation span).
+ */
+ MONGODB_COMMAND {
@Override
public String getName() {
- return "mongodb";
+ return "mongodb.command";
}
@Override
public KeyName[] getLowCardinalityKeyNames() {
- return LowCardinalityKeyNames.values();
+ return CommandLowCardinalityKeyNames.values();
}
@Override
public KeyName[] getHighCardinalityKeyNames() {
return HighCardinalityKeyNames.values();
}
-
};
/**
- * Enums related to low cardinality key names for MongoDB tags.
+ * Low cardinality key names for operation-level observations.
*/
- public enum LowCardinalityKeyNames implements KeyName {
+ public enum OperationLowCardinalityKeyNames implements KeyName {
SYSTEM {
@Override
public String asString() {
- return "db.system";
+ return "db.system.name";
}
},
NAMESPACE {
@@ -74,22 +97,41 @@ public String asString() {
return "db.operation.name";
}
},
- COMMAND_NAME {
+ OPERATION_SUMMARY {
@Override
public String asString() {
- return "db.command.name";
+ return "db.operation.summary";
+ }
+ }
+ }
+
+ /**
+ * Low cardinality key names for command-level observations.
+ */
+ public enum CommandLowCardinalityKeyNames implements KeyName {
+
+ SYSTEM {
+ @Override
+ public String asString() {
+ return "db.system.name";
}
},
- NETWORK_TRANSPORT {
+ NAMESPACE {
@Override
public String asString() {
- return "network.transport";
+ return "db.namespace";
}
},
- OPERATION_SUMMARY {
+ COLLECTION {
@Override
public String asString() {
- return "db.operation.summary";
+ return "db.collection.name";
+ }
+ },
+ COMMAND_NAME {
+ @Override
+ public String asString() {
+ return "db.command.name";
}
},
QUERY_SUMMARY {
@@ -98,10 +140,10 @@ public String asString() {
return "db.query.summary";
}
},
- CURSOR_ID {
+ NETWORK_TRANSPORT {
@Override
public String asString() {
- return "db.mongodb.cursor_id";
+ return "network.transport";
}
},
SERVER_ADDRESS {
@@ -116,6 +158,25 @@ public String asString() {
return "server.port";
}
},
+ RESPONSE_STATUS_CODE {
+ @Override
+ public String asString() {
+ return "db.response.status_code";
+ }
+ }
+ }
+
+ /**
+ * High cardinality (highly variable values) key names for command-level observations.
+ */
+ public enum HighCardinalityKeyNames implements KeyName {
+
+ QUERY_TEXT {
+ @Override
+ public String asString() {
+ return "db.query.text";
+ }
+ },
CLIENT_CONNECTION_ID {
@Override
public String asString() {
@@ -128,6 +189,12 @@ public String asString() {
return "db.mongodb.server_connection_id";
}
},
+ CURSOR_ID {
+ @Override
+ public String asString() {
+ return "db.mongodb.cursor_id";
+ }
+ },
TRANSACTION_NUMBER {
@Override
public String asString() {
@@ -140,10 +207,10 @@ public String asString() {
return "db.mongodb.lsid";
}
},
- EXCEPTION_STACKTRACE {
+ EXCEPTION_MESSAGE {
@Override
public String asString() {
- return "exception.stacktrace";
+ return "exception.message";
}
},
EXCEPTION_TYPE {
@@ -152,29 +219,10 @@ public String asString() {
return "exception.type";
}
},
- EXCEPTION_MESSAGE {
- @Override
- public String asString() {
- return "exception.message";
- }
- },
- RESPONSE_STATUS_CODE {
- @Override
- public String asString() {
- return "db.response.status_code";
- }
- }
- }
-
- /**
- * Enums related to high cardinality (highly variable values) key names for MongoDB tags.
- */
- public enum HighCardinalityKeyNames implements KeyName {
-
- QUERY_TEXT {
+ EXCEPTION_STACKTRACE {
@Override
public String asString() {
- return "db.query.text";
+ return "exception.stacktrace";
}
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java
index 84bdbb41672..d060c82b0d4 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Span.java
@@ -18,8 +18,7 @@
import com.mongodb.MongoNamespace;
import com.mongodb.lang.Nullable;
-import io.micrometer.common.KeyValue;
-import io.micrometer.common.KeyValues;
+import com.mongodb.observability.micrometer.MongodbObservationContext;
import org.bson.BsonDocument;
/**
@@ -48,15 +47,15 @@ public interface Span {
*/
Span EMPTY = new Span() {
@Override
- public void tagLowCardinality(final KeyValue tag) {
+ public void openScope() {
}
@Override
- public void tagLowCardinality(final KeyValues keyValues) {
+ public void closeScope() {
}
@Override
- public void tagHighCardinality(final String keyName, final BsonDocument value) {
+ public void setQueryText(final BsonDocument commandDocument) {
}
@Override
@@ -81,29 +80,32 @@ public TraceContext context() {
public MongoNamespace getNamespace() {
return null;
}
+
+ @Override
+ @Nullable
+ public MongodbObservationContext getMongodbObservationContext() {
+ return null;
+ }
};
/**
- * Adds a low-cardinality tag to the span.
- *
- * @param keyValue The key-value pair representing the tag.
+ * Opens a scope for this span, making it the current observation on the thread.
+ * Must be paired with {@link #closeScope()} in a try-finally block.
*/
- void tagLowCardinality(KeyValue keyValue);
+ void openScope();
/**
- * Adds multiple low-cardinality tags to the span.
- *
- * @param keyValues The key-value pairs representing the tags.
+ * Closes the scope previously opened by {@link #openScope()}, restoring the previous observation.
*/
- void tagLowCardinality(KeyValues keyValues);
+ void closeScope();
/**
- * Adds a high-cardinality (highly variable values) tag to the span with a BSON document value.
+ * Sets the query text on the observation context from the given command document.
+ * The document is converted to a JSON string and may be truncated based on configuration.
*
- * @param keyName The name of the tag.
- * @param value The BSON document representing the value of the tag.
+ * @param commandDocument The BSON command document.
*/
- void tagHighCardinality(String keyName, BsonDocument value);
+ void setQueryText(BsonDocument commandDocument);
/**
* Records an event in the span.
@@ -131,6 +133,15 @@ public MongoNamespace getNamespace() {
*/
TraceContext context();
+ /**
+ * Retrieves the {@link MongodbObservationContext} associated with the span, if any.
+ * Returns null for no-op spans or non-Micrometer implementations.
+ *
+ * @return The MongoDB observation context, or null.
+ */
+ @Nullable
+ MongodbObservationContext getMongodbObservationContext();
+
/**
* Retrieves the MongoDB namespace associated with the span, if any.
*
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java
index 632580ab40e..fc1ddca68d0 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/Tracer.java
@@ -30,7 +30,8 @@
public interface Tracer {
Tracer NO_OP = new Tracer() {
@Override
- public Span nextSpan(final String name, @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) {
+ public Span nextSpan(final MongodbObservation observationType, final String name,
+ @Nullable final TraceContext parent, @Nullable final MongoNamespace namespace) {
return Span.EMPTY;
}
@@ -46,14 +47,15 @@ public boolean includeCommandPayload() {
};
/**
- * Creates a new span with the specified name and optional parent trace context.
+ * Creates a new span with the specified observation type, name and optional parent trace context.
*
+ * @param observationType The {@link MongodbObservation} type (operation or command).
* @param name The name of the span.
* @param parent The parent {@link TraceContext}, or null if no parent context is provided.
* @param namespace The {@link MongoNamespace} associated with the span, or null if none is provided.
* @return A {@link Span} representing the newly created span.
*/
- Span nextSpan(String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace);
+ Span nextSpan(MongodbObservation observationType, String name, @Nullable TraceContext parent, @Nullable MongoNamespace namespace);
/**
* Indicates whether tracing is enabled.
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java
index 4247ed1c3dd..47347de8482 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java
@@ -26,29 +26,17 @@
import com.mongodb.internal.session.SessionContext;
import com.mongodb.lang.Nullable;
import com.mongodb.observability.ObservabilitySettings;
+import com.mongodb.observability.micrometer.DefaultMongodbObservationConvention;
import com.mongodb.observability.micrometer.MicrometerObservabilitySettings;
-import io.micrometer.common.KeyValues;
+import com.mongodb.observability.micrometer.MongodbObservationContext;
import io.micrometer.observation.ObservationRegistry;
import org.bson.BsonDocument;
import java.util.function.Predicate;
import java.util.function.Supplier;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CLIENT_CONNECTION_ID;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COLLECTION;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.COMMAND_NAME;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NAMESPACE;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_NAME;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.OPERATION_SUMMARY;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.NETWORK_TRANSPORT;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.QUERY_SUMMARY;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_ADDRESS;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_CONNECTION_ID;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SERVER_PORT;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SESSION_ID;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.SYSTEM;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.TRANSACTION_NUMBER;
-import static com.mongodb.internal.observability.micrometer.MongodbObservation.LowCardinalityKeyNames.CURSOR_ID;
+import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_COMMAND;
+import static com.mongodb.internal.observability.micrometer.MongodbObservation.MONGODB_OPERATION;
import static java.lang.System.getenv;
/**
@@ -101,7 +89,8 @@ public TracingManager(@Nullable final ObservabilitySettings observabilitySetting
ObservationRegistry observationRegistry = settings.getObservationRegistry();
tracer = enableTracing && observationRegistry != null
- ? new MicrometerTracer(observationRegistry, settings.isEnableCommandPayloadTracing(), settings.getMaxQueryTextLength())
+ ? new MicrometerTracer(observationRegistry, settings.isEnableCommandPayloadTracing(),
+ settings.getMaxQueryTextLength(), settings.getObservationConvention())
: Tracer.NO_OP;
this.enableCommandPayload = tracer.includeCommandPayload();
@@ -109,35 +98,31 @@ public TracingManager(@Nullable final ObservabilitySettings observabilitySetting
}
/**
- * Creates a new span with the specified name and parent trace context.
- *
- * This method is used to create a span that is linked to a parent context,
- * enabling hierarchical tracing of operations.
- *
+ * Creates a new span with the specified observation type, name and parent trace context.
*
- * @param name The name of the span.
- * @param parentContext The parent trace context to associate with the span.
+ * @param observationType The observation type (operation or command).
+ * @param name The name of the span.
+ * @param parentContext The parent trace context to associate with the span.
* @return The created span.
*/
- public Span addSpan(final String name, @Nullable final TraceContext parentContext) {
- return tracer.nextSpan(name, parentContext, null);
+ public Span addSpan(final MongodbObservation observationType, final String name,
+ @Nullable final TraceContext parentContext) {
+ return tracer.nextSpan(observationType, name, parentContext, null);
}
/**
- * Creates a new span with the specified name, parent trace context, and MongoDB namespace.
- *
- * This method is used to create a span that is linked to a parent context,
- * enabling hierarchical tracing of operations. The MongoDB namespace can be used
- * by nested spans to access the database and collection name (which might not be easily accessible at connection layer).
- *
+ * Creates a new span with the specified observation type, name, parent trace context,
+ * and MongoDB namespace.
*
- * @param name The name of the span.
- * @param parentContext The parent trace context to associate with the span.
- * @param namespace The MongoDB namespace associated with the operation.
+ * @param observationType The observation type (operation or command).
+ * @param name The name of the span.
+ * @param parentContext The parent trace context to associate with the span.
+ * @param namespace The MongoDB namespace associated with the operation.
* @return The created span.
*/
- public Span addSpan(final String name, @Nullable final TraceContext parentContext, final MongoNamespace namespace) {
- return tracer.nextSpan(name, parentContext, namespace);
+ public Span addSpan(final MongodbObservation observationType, final String name,
+ @Nullable final TraceContext parentContext, final MongoNamespace namespace) {
+ return tracer.nextSpan(observationType, name, parentContext, namespace);
}
/**
@@ -146,9 +131,7 @@ public Span addSpan(final String name, @Nullable final TraceContext parentContex
* @return The created transaction span.
*/
public Span addTransactionSpan() {
- Span span = tracer.nextSpan("transaction", null, null);
- span.tagLowCardinality(SYSTEM.withValue("mongodb"));
- return span;
+ return tracer.nextSpan(MONGODB_OPERATION, "transaction", null, null);
}
/**
@@ -173,10 +156,10 @@ public boolean isCommandPayloadEnabled() {
/** Create a tracing span for the given command message.
*
* The span is only created if tracing is enabled and the command is not security-sensitive.
- * It attaches various tags to the span, such as database system, namespace, query summary, opcode,
- * server address, port, server type, client and server connection IDs, and, if applicable,
- * transaction number and session ID.
- * If command payload tracing is enabled, the command document is also attached as a tag.
+ * It populates domain fields on the span's {@link MongodbObservationContext} (command name, namespace,
+ * server address, connection ID, session/transaction info, cursor ID for getMore commands).
+ * The {@link DefaultMongodbObservationConvention} reads these fields at observation stop time
+ * to produce the final tag key-values.
*
* @param message the command message to trace
* @param operationContext the operation context containing tracing and session information
@@ -205,17 +188,9 @@ public Span createTracingSpan(final CommandMessage message,
}
Span operationSpan = operationContext.getTracingSpan();
- Span span = addSpan(commandName, operationSpan != null ? operationSpan.context() : null);
-
- if (command.containsKey("getMore")) {
- long cursorId = command.getInt64("getMore").longValue();
- span.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId)));
- if (operationSpan != null) {
- operationSpan.tagLowCardinality(CURSOR_ID.withValue(String.valueOf(cursorId)));
- }
- }
+ Span span = addSpan(MONGODB_COMMAND, commandName, operationSpan != null ? operationSpan.context() : null);
- // Tag namespace
+ // Resolve namespace from parent operation span or message
String namespace;
String collection = "";
if (operationSpan != null) {
@@ -231,39 +206,35 @@ public Span createTracingSpan(final CommandMessage message,
} else {
namespace = message.getDatabase();
}
- String summary = commandName + " " + namespace + (collection.isEmpty() ? "" : "." + collection);
- KeyValues keyValues = KeyValues.of(
- SYSTEM.withValue("mongodb"),
- NAMESPACE.withValue(namespace),
- QUERY_SUMMARY.withValue(summary),
- COMMAND_NAME.withValue(commandName));
+ // Populate domain fields on MongodbObservationContext — the convention reads these to produce tags
+ MongodbObservationContext mongodbContext = span.getMongodbObservationContext();
+ if (mongodbContext != null) {
+ mongodbContext.setCommandName(commandName);
+ mongodbContext.setDatabaseName(namespace);
+ if (!collection.isEmpty()) {
+ mongodbContext.setCollectionName(collection);
+ }
- if (!collection.isEmpty()) {
- keyValues = keyValues.and(COLLECTION.withValue(collection));
- }
- span.tagLowCardinality(keyValues);
-
- // tag server and connection info
- ServerAddress serverAddress = serverAddressSupplier.get();
- ConnectionId connectionId = connectionIdSupplier.get();
- span.tagLowCardinality(KeyValues.of(
- SERVER_ADDRESS.withValue(serverAddress.getHost()),
- SERVER_PORT.withValue(String.valueOf(serverAddress.getPort())),
- CLIENT_CONNECTION_ID.withValue(String.valueOf(connectionId.getLocalValue())),
- SERVER_CONNECTION_ID.withValue(String.valueOf(connectionId.getServerValue())),
- NETWORK_TRANSPORT.withValue(serverAddress instanceof UnixServerAddress ? "unix" : "tcp")
- ));
-
- // tag session and transaction info
- SessionContext sessionContext = operationContext.getSessionContext();
- if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) {
- span.tagLowCardinality(KeyValues.of(
- TRANSACTION_NUMBER.withValue(String.valueOf(sessionContext.getTransactionNumber())),
- SESSION_ID.withValue(String.valueOf(sessionContext.getSessionId()
- .get(sessionContext.getSessionId().getFirstKey())
- .asBinary().asUuid()))
- ));
+ ServerAddress serverAddress = serverAddressSupplier.get();
+ mongodbContext.setServerAddress(serverAddress);
+ mongodbContext.setUnixSocket(serverAddress instanceof UnixServerAddress);
+
+ ConnectionId connectionId = connectionIdSupplier.get();
+ mongodbContext.setConnectionId(connectionId);
+
+ if (command.containsKey("getMore")) {
+ long cursorId = command.getInt64("getMore").longValue();
+ mongodbContext.setCursorId(cursorId);
+ }
+
+ SessionContext sessionContext = operationContext.getSessionContext();
+ if (sessionContext.hasSession() && !sessionContext.isImplicitSession()) {
+ mongodbContext.setTransactionNumber(sessionContext.getTransactionNumber());
+ mongodbContext.setSessionId(String.valueOf(sessionContext.getSessionId()
+ .get(sessionContext.getSessionId().getFirstKey())
+ .asBinary().asUuid()));
+ }
}
return span;
@@ -297,17 +268,18 @@ public Span createOperationSpan(@Nullable final TransactionSpan transactionSpan,
? ""
: "." + namespace.getCollectionName());
- KeyValues keyValues = KeyValues.of(
- SYSTEM.withValue("mongodb"),
- NAMESPACE.withValue(namespace.getDatabaseName()));
- if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) {
- keyValues = keyValues.and(COLLECTION.withValue(namespace.getCollectionName()));
+ Span span = addSpan(MONGODB_OPERATION, name, parentContext, namespace);
+
+ // Populate domain fields on MongodbObservationContext — the convention reads these to produce tags
+ MongodbObservationContext mongodbContext = span.getMongodbObservationContext();
+ if (mongodbContext != null) {
+ mongodbContext.setCommandName(commandName);
+ mongodbContext.setDatabaseName(namespace.getDatabaseName());
+ if (!MongoNamespaceHelper.COMMAND_COLLECTION_NAME.equalsIgnoreCase(namespace.getCollectionName())) {
+ mongodbContext.setCollectionName(namespace.getCollectionName());
+ }
}
- keyValues = keyValues.and(OPERATION_NAME.withValue(commandName),
- OPERATION_SUMMARY.withValue(name));
- Span span = addSpan(name, parentContext, namespace);
- span.tagLowCardinality(keyValues);
operationContext.setTracingSpan(span);
return span;
}
diff --git a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java
index 3d16d18a976..03a6301b02b 100644
--- a/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java
+++ b/driver-core/src/main/com/mongodb/internal/observability/micrometer/TransactionSpan.java
@@ -54,6 +54,7 @@ public void handleTransactionSpanError(final Throwable e) {
}
if (!isConvenientTransaction) {
+ span.closeScope();
span.end();
}
}
@@ -67,6 +68,7 @@ public void finalizeTransactionSpan(final String status) {
span.event(status);
// clear previous commit error if any
if (!isConvenientTransaction) {
+ span.closeScope();
span.end();
}
reportedError = null; // clear previous commit error if any
@@ -82,6 +84,7 @@ public void spanFinalizing(final boolean cleanupTransactionContext) {
if (reportedError != null) {
span.error(reportedError);
}
+ span.closeScope();
span.end();
reportedError = null;
// Don't clean up transaction context if we're still retrying (we want the retries to fold under the original transaction span)
@@ -109,4 +112,12 @@ public void setIsConvenientTransaction() {
public TraceContext getContext() {
return span.context();
}
+
+ /**
+ * Opens a scope for the transaction span, making it the current observation on the thread.
+ * Must only be called from the sync driver where open and close happen on the same thread.
+ */
+ public void openScope() {
+ span.openScope();
+ }
}
diff --git a/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java b/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java
index 7cc3374cbf1..a9e6e43c9c9 100644
--- a/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java
+++ b/driver-core/src/main/com/mongodb/observability/ObservabilitySettings.java
@@ -16,7 +16,7 @@
package com.mongodb.observability;
-import com.mongodb.annotations.Alpha;
+import com.mongodb.annotations.Beta;
import com.mongodb.annotations.Immutable;
import com.mongodb.annotations.Reason;
import com.mongodb.annotations.Sealed;
@@ -27,7 +27,7 @@
*
* @since 5.7
*/
-@Alpha(Reason.CLIENT)
+@Beta(Reason.CLIENT)
@Sealed
@Immutable
public abstract class ObservabilitySettings {
diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java
new file mode 100644
index 00000000000..fbce064a04f
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/observability/micrometer/DefaultMongodbObservationConvention.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.observability.micrometer;
+
+import com.mongodb.ServerAddress;
+import com.mongodb.annotations.Beta;
+import com.mongodb.annotations.Reason;
+import com.mongodb.connection.ConnectionId;
+import com.mongodb.internal.observability.micrometer.MongodbObservation;
+import io.micrometer.common.KeyValues;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Default {@link ObservationConvention} for MongoDB observations.
+ *
+ * Reads domain fields from {@link MongodbObservationContext} and produces the standard MongoDB
+ * low-cardinality and high-cardinality key-values. Users can provide a custom convention via
+ * {@link MicrometerObservabilitySettings.Builder#observationConvention(ObservationConvention)}.
+ *
+ *
+ * @since 5.7
+ */
+@Beta(Reason.CLIENT)
+public class DefaultMongodbObservationConvention implements ObservationConvention {
+
+ @Override
+ public boolean supportsContext(final Observation.Context context) {
+ return context instanceof MongodbObservationContext;
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(final MongodbObservationContext context) {
+ if (context.getObservationType() == MongodbObservation.MONGODB_OPERATION) {
+ return getOperationLowCardinalityKeyValues(context);
+ } else {
+ return getCommandLowCardinalityKeyValues(context);
+ }
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(final MongodbObservationContext context) {
+ if (context.getObservationType() == MongodbObservation.MONGODB_COMMAND) {
+ return getCommandHighCardinalityKeyValues(context);
+ }
+ return KeyValues.empty();
+ }
+
+ private KeyValues getOperationLowCardinalityKeyValues(final MongodbObservationContext context) {
+ String commandName = context.getCommandName();
+ String databaseName = context.getDatabaseName();
+ String collectionName = context.getCollectionName();
+
+ KeyValues kv = KeyValues.of(
+ MongodbObservation.OperationLowCardinalityKeyNames.SYSTEM.withValue("mongodb"));
+
+ if (databaseName != null) {
+ kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.NAMESPACE.withValue(databaseName));
+ }
+ if (collectionName != null) {
+ kv = kv.and(MongodbObservation.OperationLowCardinalityKeyNames.COLLECTION.withValue(collectionName));
+ }
+ if (commandName != null) {
+ String dbName = databaseName != null ? databaseName : "";
+ String summary = commandName + " " + dbName
+ + (collectionName != null ? "." + collectionName : "");
+ kv = kv.and(
+ MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_NAME.withValue(commandName),
+ MongodbObservation.OperationLowCardinalityKeyNames.OPERATION_SUMMARY.withValue(summary));
+ }
+ return kv;
+ }
+
+ private KeyValues getCommandLowCardinalityKeyValues(final MongodbObservationContext context) {
+ String commandName = context.getCommandName();
+ String databaseName = context.getDatabaseName();
+ String collectionName = context.getCollectionName();
+ String cmdName = commandName != null ? commandName : "";
+ String dbName = databaseName != null ? databaseName : "";
+ String summary = cmdName + " " + dbName
+ + (collectionName != null ? "." + collectionName : "");
+
+ KeyValues kv = KeyValues.of(
+ MongodbObservation.CommandLowCardinalityKeyNames.SYSTEM.withValue("mongodb"),
+ MongodbObservation.CommandLowCardinalityKeyNames.NAMESPACE.withValue(dbName),
+ MongodbObservation.CommandLowCardinalityKeyNames.QUERY_SUMMARY.withValue(summary),
+ MongodbObservation.CommandLowCardinalityKeyNames.COMMAND_NAME.withValue(cmdName));
+ if (collectionName != null) {
+ kv = kv.and(MongodbObservation.CommandLowCardinalityKeyNames.COLLECTION.withValue(collectionName));
+ }
+ ServerAddress serverAddress = context.getServerAddress();
+ if (serverAddress != null) {
+ kv = kv.and(
+ MongodbObservation.CommandLowCardinalityKeyNames.SERVER_ADDRESS.withValue(serverAddress.getHost()),
+ MongodbObservation.CommandLowCardinalityKeyNames.SERVER_PORT.withValue(
+ String.valueOf(serverAddress.getPort())),
+ MongodbObservation.CommandLowCardinalityKeyNames.NETWORK_TRANSPORT.withValue(
+ context.isUnixSocket() ? "unix" : "tcp"));
+ }
+ String responseStatusCode = context.getResponseStatusCode();
+ if (responseStatusCode != null) {
+ kv = kv.and(MongodbObservation.CommandLowCardinalityKeyNames.RESPONSE_STATUS_CODE.withValue(responseStatusCode));
+ }
+ return kv;
+ }
+
+ private KeyValues getCommandHighCardinalityKeyValues(final MongodbObservationContext context) {
+ KeyValues kv = KeyValues.empty();
+
+ String queryText = context.getQueryText();
+ if (queryText != null) {
+ kv = kv.and(MongodbObservation.HighCardinalityKeyNames.QUERY_TEXT.withValue(queryText));
+ }
+ ConnectionId connectionId = context.getConnectionId();
+ if (connectionId != null) {
+ kv = kv.and(
+ MongodbObservation.HighCardinalityKeyNames.CLIENT_CONNECTION_ID.withValue(
+ String.valueOf(connectionId.getLocalValue())),
+ MongodbObservation.HighCardinalityKeyNames.SERVER_CONNECTION_ID.withValue(
+ String.valueOf(connectionId.getServerValue())));
+ }
+ Long cursorId = context.getCursorId();
+ if (cursorId != null) {
+ kv = kv.and(MongodbObservation.HighCardinalityKeyNames.CURSOR_ID.withValue(
+ String.valueOf(cursorId)));
+ }
+ Long transactionNumber = context.getTransactionNumber();
+ if (transactionNumber != null) {
+ kv = kv.and(MongodbObservation.HighCardinalityKeyNames.TRANSACTION_NUMBER.withValue(
+ String.valueOf(transactionNumber)));
+ }
+ String sessionId = context.getSessionId();
+ if (sessionId != null) {
+ kv = kv.and(MongodbObservation.HighCardinalityKeyNames.SESSION_ID.withValue(sessionId));
+ }
+
+ // Exception tags from observation error
+ Throwable error = context.getError();
+ if (error != null) {
+ kv = kv.and(
+ MongodbObservation.HighCardinalityKeyNames.EXCEPTION_MESSAGE.withValue(String.valueOf(error.getMessage())),
+ MongodbObservation.HighCardinalityKeyNames.EXCEPTION_TYPE.withValue(error.getClass().getName()),
+ MongodbObservation.HighCardinalityKeyNames.EXCEPTION_STACKTRACE.withValue(getStackTraceAsString(error)));
+ }
+
+ return kv;
+ }
+
+ private static String getStackTraceAsString(final Throwable throwable) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw);
+ return sw.toString();
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java b/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java
index 426e4671185..ad663c063b5 100644
--- a/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java
+++ b/driver-core/src/main/com/mongodb/observability/micrometer/MicrometerObservabilitySettings.java
@@ -17,12 +17,13 @@
package com.mongodb.observability.micrometer;
import com.mongodb.MongoConfigurationException;
-import com.mongodb.annotations.Alpha;
+import com.mongodb.annotations.Beta;
import com.mongodb.annotations.Immutable;
import com.mongodb.annotations.NotThreadSafe;
import com.mongodb.annotations.Reason;
import com.mongodb.lang.Nullable;
import com.mongodb.observability.ObservabilitySettings;
+import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
import java.util.Objects;
@@ -44,7 +45,7 @@
*
* @since 5.7
*/
-@Alpha(Reason.CLIENT)
+@Beta(Reason.CLIENT)
@Immutable
public final class MicrometerObservabilitySettings extends ObservabilitySettings {
@@ -64,6 +65,8 @@ public final class MicrometerObservabilitySettings extends ObservabilitySettings
private final ObservationRegistry observationRegistry;
private final int maxQueryTextLength;
private final boolean enableCommandPayloadTracing;
+ @Nullable
+ private final ObservationConvention observationConvention;
/**
* Convenience method to create a Builder.
@@ -99,6 +102,14 @@ public boolean isEnableCommandPayloadTracing() {
return enableCommandPayloadTracing;
}
+ /**
+ * @return the observation convention, or null to use the default
+ */
+ @Nullable
+ public ObservationConvention getObservationConvention() {
+ return observationConvention;
+ }
+
/**
* @return the maximum length of command payloads captured in tracing spans.
*/
@@ -115,6 +126,8 @@ public static final class Builder {
private ObservationRegistry observationRegistry;
private boolean enableCommandPayloadTracing;
private int maxQueryTextLength = Integer.MAX_VALUE;
+ @Nullable
+ private ObservationConvention observationConvention;
private Builder() {
if (!OBSERVATION_REGISTRY_AVAILABLE) {
@@ -126,6 +139,7 @@ private Builder(final MicrometerObservabilitySettings settings) {
this.observationRegistry = settings.observationRegistry;
this.enableCommandPayloadTracing = settings.enableCommandPayloadTracing;
this.maxQueryTextLength = settings.maxQueryTextLength;
+ this.observationConvention = settings.observationConvention;
}
/**
@@ -141,6 +155,7 @@ public MicrometerObservabilitySettings.Builder applySettings(final MicrometerObs
observationRegistry = settings.observationRegistry;
enableCommandPayloadTracing = settings.enableCommandPayloadTracing;
maxQueryTextLength = settings.maxQueryTextLength;
+ observationConvention = settings.observationConvention;
return this;
}
@@ -151,7 +166,7 @@ public MicrometerObservabilitySettings.Builder applySettings(final MicrometerObs
* @return this
* @since 5.7
*/
- @Alpha(Reason.CLIENT)
+ @Beta(Reason.CLIENT)
public Builder observationRegistry(@Nullable final ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
return this;
@@ -165,7 +180,7 @@ public Builder observationRegistry(@Nullable final ObservationRegistry observati
* @return this
* @since 5.7
*/
- @Alpha(Reason.CLIENT)
+ @Beta(Reason.CLIENT)
public Builder enableCommandPayloadTracing(final boolean enableCommandPayload) {
this.enableCommandPayloadTracing = enableCommandPayload;
return this;
@@ -178,17 +193,32 @@ public Builder enableCommandPayloadTracing(final boolean enableCommandPayload) {
* @return this
* @since 5.7
*/
- @Alpha(Reason.CLIENT)
+ @Beta(Reason.CLIENT)
public Builder maxQueryTextLength(final int maxQueryTextLength) {
this.maxQueryTextLength = maxQueryTextLength;
return this;
}
+ /**
+ * Sets a custom {@link ObservationConvention} to control the tag names and values produced by MongoDB observations.
+ * If not set, the driver uses {@link DefaultMongodbObservationConvention}.
+ *
+ * @param observationConvention the custom convention, or null to use the default
+ * @return this
+ * @since 5.7
+ */
+ @Beta(Reason.CLIENT)
+ public Builder observationConvention(@Nullable final ObservationConvention observationConvention) {
+ this.observationConvention = observationConvention;
+ return this;
+ }
+
/**
* @return the configured settings
*/
public MicrometerObservabilitySettings build() {
- return new MicrometerObservabilitySettings(observationRegistry, enableCommandPayloadTracing, maxQueryTextLength);
+ return new MicrometerObservabilitySettings(observationRegistry, enableCommandPayloadTracing, maxQueryTextLength,
+ observationConvention);
}
}
@@ -199,18 +229,21 @@ public boolean equals(final Object o) {
}
final MicrometerObservabilitySettings that = (MicrometerObservabilitySettings) o;
return enableCommandPayloadTracing == that.enableCommandPayloadTracing
- && Objects.equals(observationRegistry, that.observationRegistry);
+ && Objects.equals(observationRegistry, that.observationRegistry)
+ && Objects.equals(observationConvention, that.observationConvention);
}
@Override
public int hashCode() {
- return Objects.hash(observationRegistry, enableCommandPayloadTracing);
+ return Objects.hash(observationRegistry, enableCommandPayloadTracing, observationConvention);
}
private MicrometerObservabilitySettings(@Nullable final ObservationRegistry observationRegistry,
- final boolean enableCommandPayloadTracing, final int maxQueryTextLength) {
+ final boolean enableCommandPayloadTracing, final int maxQueryTextLength,
+ @Nullable final ObservationConvention observationConvention) {
this.observationRegistry = observationRegistry;
this.enableCommandPayloadTracing = enableCommandPayloadTracing;
this.maxQueryTextLength = maxQueryTextLength;
+ this.observationConvention = observationConvention;
}
}
diff --git a/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java
new file mode 100644
index 00000000000..cbbd12a2e8d
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/observability/micrometer/MongodbObservationContext.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.observability.micrometer;
+
+import com.mongodb.ServerAddress;
+import com.mongodb.annotations.Beta;
+import com.mongodb.annotations.Reason;
+import com.mongodb.connection.ConnectionId;
+import com.mongodb.internal.observability.micrometer.MongodbObservation;
+import com.mongodb.lang.Nullable;
+import io.micrometer.observation.transport.Kind;
+import io.micrometer.observation.transport.SenderContext;
+
+/**
+ * A MongoDB-specific {@link SenderContext} for Micrometer observations.
+ *
+ * Extends {@link SenderContext} with {@link Kind#CLIENT} to preserve the client span kind
+ * in the tracing bridge. Provides a MongoDB-specific type that users can filter on
+ * when registering {@code ObservationHandler} or {@code ObservationConvention} instances.
+ *
+ *
+ * Domain fields (commandName, databaseName, etc.) are populated by the driver after
+ * the observation is started and before it is stopped. The {@code ObservationConvention}
+ * reads these fields at stop time to produce the final tag key-values.
+ *