-
Notifications
You must be signed in to change notification settings - Fork 1.5k
JAVA-6222 Support trace context propagation to the server #2022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f991058
71944e9
c23e2c9
b7def2e
10a7e48
22460aa
eb27cfa
44fd4ca
9bd5c65
9931e89
d943d9e
0176242
321b7c3
48d76fc
c36118c
9bf631a
f067ee1
9d7cfd6
4c3715f
fe04efd
346a959
e295fa0
6db604c
7352282
b3e5358
dc9fa67
80b0c2f
4c5e5dc
8cade28
7369f1f
e34ba1f
fc850bf
1bd5edf
746de73
b4c8e8c
6c709f7
8f3940a
7ecaf57
b9f56be
b96eef9
baaa522
019f53d
2a31215
8f3db54
f1a4d02
ca3beb0
4f417ff
2ce9337
73cf17b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |||||||||||||||||
| import com.mongodb.internal.MongoNamespaceHelper; | ||||||||||||||||||
| import com.mongodb.internal.TimeoutContext; | ||||||||||||||||||
| import com.mongodb.internal.connection.MessageSequences.EmptyMessageSequences; | ||||||||||||||||||
| import com.mongodb.internal.observability.micrometer.Span; | ||||||||||||||||||
| import com.mongodb.internal.session.SessionContext; | ||||||||||||||||||
| import com.mongodb.lang.Nullable; | ||||||||||||||||||
| import org.bson.BsonArray; | ||||||||||||||||||
|
|
@@ -34,6 +35,7 @@ | |||||||||||||||||
| import org.bson.BsonElement; | ||||||||||||||||||
| import org.bson.BsonInt64; | ||||||||||||||||||
| import org.bson.BsonString; | ||||||||||||||||||
| import org.bson.BsonValue; | ||||||||||||||||||
| import org.bson.ByteBuf; | ||||||||||||||||||
| import org.bson.FieldNameValidator; | ||||||||||||||||||
| import org.bson.io.BsonOutput; | ||||||||||||||||||
|
|
@@ -64,6 +66,7 @@ | |||||||||||||||||
| import static com.mongodb.internal.connection.ByteBufBsonDocument.createList; | ||||||||||||||||||
| import static com.mongodb.internal.connection.ByteBufBsonDocument.createOne; | ||||||||||||||||||
| import static com.mongodb.internal.connection.ReadConcernHelper.getReadConcernDocument; | ||||||||||||||||||
| import static com.mongodb.internal.operation.ServerVersionHelper.NINE_DOT_ZERO_WIRE_VERSION; | ||||||||||||||||||
| import static com.mongodb.internal.operation.ServerVersionHelper.UNKNOWN_WIRE_VERSION; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
|
|
@@ -80,6 +83,11 @@ public final class CommandMessage extends RequestMessage { | |||||||||||||||||
| * Specifies that the `OP_MSG` section payload is a sequence of BSON documents. | ||||||||||||||||||
| */ | ||||||||||||||||||
| private static final byte PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE = 1; | ||||||||||||||||||
| /** | ||||||||||||||||||
| * Specifies that the `OP_MSG` section payload is a BSON telemetry document | ||||||||||||||||||
| * ({@code {otel: {traceparent: <string>}}}). | ||||||||||||||||||
| */ | ||||||||||||||||||
| private static final byte PAYLOAD_TYPE_3_TELEMETRY = 3; | ||||||||||||||||||
|
|
||||||||||||||||||
| private static final int UNINITIALIZED_POSITION = -1; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -101,6 +109,14 @@ public final class CommandMessage extends RequestMessage { | |||||||||||||||||
| private final ClusterConnectionMode clusterConnectionMode; | ||||||||||||||||||
| private final ServerApi serverApi; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * The command span to attach to the OP_MSG telemetry section during {@linkplain | ||||||||||||||||||
| * #encode(ByteBufferBsonOutput, OperationContext, Span) encoding}. Set for the duration of that overload's | ||||||||||||||||||
| * call and cleared afterward; {@code null} otherwise. | ||||||||||||||||||
| */ | ||||||||||||||||||
| @Nullable | ||||||||||||||||||
| private Span commandSpanForEncoding; | ||||||||||||||||||
|
|
||||||||||||||||||
| CommandMessage(final String database, final BsonDocument command, final FieldNameValidator commandFieldNameValidator, | ||||||||||||||||||
| final ReadPreference readPreference, final MessageSettings settings, final ClusterConnectionMode clusterConnectionMode, | ||||||||||||||||||
| @Nullable final ServerApi serverApi) { | ||||||||||||||||||
|
|
@@ -156,36 +172,42 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { | |||||||||||||||||
| byteBuf.position(firstDocumentPosition); | ||||||||||||||||||
| ByteBufBsonDocument byteBufBsonDocument = createOne(byteBuf); | ||||||||||||||||||
|
|
||||||||||||||||||
| // If true, it means there is at least one `PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE` section in the OP_MSG | ||||||||||||||||||
| if (byteBuf.hasRemaining()) { | ||||||||||||||||||
| BsonDocument commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Each loop iteration processes one Document Sequence | ||||||||||||||||||
| // When there are no more bytes remaining, there are no more Document Sequences | ||||||||||||||||||
| while (byteBuf.hasRemaining()) { | ||||||||||||||||||
| // skip reading the payload type, we know it is `PAYLOAD_TYPE_1` | ||||||||||||||||||
| byteBuf.position(byteBuf.position() + 1); | ||||||||||||||||||
| int sequenceStart = byteBuf.position(); | ||||||||||||||||||
| int sequenceSizeInBytes = byteBuf.getInt(); | ||||||||||||||||||
| int sectionEnd = sequenceStart + sequenceSizeInBytes; | ||||||||||||||||||
|
|
||||||||||||||||||
| String fieldName = getSequenceIdentifier(byteBuf); | ||||||||||||||||||
| // If this assertion fires, it means that the driver has started using document sequences for nested fields. If | ||||||||||||||||||
| // so, this method will need to change in order to append the value to the correct nested document. | ||||||||||||||||||
| assertFalse(fieldName.contains(".")); | ||||||||||||||||||
|
|
||||||||||||||||||
| ByteBuf documentsByteBufSlice = byteBuf.duplicate().limit(sectionEnd); | ||||||||||||||||||
| try { | ||||||||||||||||||
| commandBsonDocument.append(fieldName, new BsonArray(createList(documentsByteBufSlice))); | ||||||||||||||||||
| } finally { | ||||||||||||||||||
| documentsByteBufSlice.release(); | ||||||||||||||||||
| } | ||||||||||||||||||
| byteBuf.position(sectionEnd); | ||||||||||||||||||
| // There may be more sections after the `PAYLOAD_TYPE_0_DOCUMENT` section: either one or more | ||||||||||||||||||
| // `PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE` sections, and/or a trailing `PAYLOAD_TYPE_3_TELEMETRY` section. | ||||||||||||||||||
| // The command document is materialized lazily, only when a document sequence has to be folded into | ||||||||||||||||||
| // it; otherwise (no sections, or only a telemetry section) the cheap lazy document is returned. | ||||||||||||||||||
| BsonDocument commandBsonDocument = null; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Each loop iteration processes one Document Sequence | ||||||||||||||||||
| // When there are no more bytes remaining, there are no more Document Sequences | ||||||||||||||||||
| while (byteBuf.hasRemaining()) { | ||||||||||||||||||
| byte payloadType = byteBuf.get(); | ||||||||||||||||||
| // Document-sequence sections always precede any trailing non-sequence section (e.g. | ||||||||||||||||||
| // PAYLOAD_TYPE_3_TELEMETRY), and such sections carry no command-document fields, so stop here. | ||||||||||||||||||
| if (payloadType != PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE) { | ||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
| if (commandBsonDocument == null) { | ||||||||||||||||||
| commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); | ||||||||||||||||||
| } | ||||||||||||||||||
| int sequenceStart = byteBuf.position(); | ||||||||||||||||||
| int sequenceSizeInBytes = byteBuf.getInt(); | ||||||||||||||||||
| int sectionEnd = sequenceStart + sequenceSizeInBytes; | ||||||||||||||||||
|
|
||||||||||||||||||
| String fieldName = getSequenceIdentifier(byteBuf); | ||||||||||||||||||
| // If this assertion fires, it means that the driver has started using document sequences for nested fields. If | ||||||||||||||||||
| // so, this method will need to change in order to append the value to the correct nested document. | ||||||||||||||||||
| assertFalse(fieldName.contains(".")); | ||||||||||||||||||
|
|
||||||||||||||||||
| ByteBuf documentsByteBufSlice = byteBuf.duplicate().limit(sectionEnd); | ||||||||||||||||||
| try { | ||||||||||||||||||
| commandBsonDocument.append(fieldName, new BsonArray(createList(documentsByteBufSlice))); | ||||||||||||||||||
| } finally { | ||||||||||||||||||
| documentsByteBufSlice.release(); | ||||||||||||||||||
| } | ||||||||||||||||||
| return commandBsonDocument; | ||||||||||||||||||
| } else { | ||||||||||||||||||
| return byteBufBsonDocument; | ||||||||||||||||||
| byteBuf.position(sectionEnd); | ||||||||||||||||||
| } | ||||||||||||||||||
| return commandBsonDocument != null ? commandBsonDocument : byteBufBsonDocument; | ||||||||||||||||||
| } finally { | ||||||||||||||||||
| byteBuf.release(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -194,6 +216,24 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { | |||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * The command name (the first key of the command document). Available before {@code encode()} runs, unlike | ||||||||||||||||||
| * {@link #getCommandDocument(ByteBufferBsonOutput)}; identical to the encoded document's first | ||||||||||||||||||
| * key, since encoding only appends fields. | ||||||||||||||||||
| */ | ||||||||||||||||||
| public String getCommandName() { | ||||||||||||||||||
| return command.getFirstKey(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * The cursor id if this is a {@code getMore} command, or {@code null} otherwise. | ||||||||||||||||||
| */ | ||||||||||||||||||
| @Nullable | ||||||||||||||||||
| public BsonInt64 getGetMoreCursorId() { | ||||||||||||||||||
| BsonValue value = command.get("getMore"); | ||||||||||||||||||
| return value instanceof BsonInt64 ? (BsonInt64) value : null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+232
to
+235
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend changing to a
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also made a suggestion below to change |
||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Get the field name from a buffer positioned at the start of the document sequence identifier of an OP_MSG Section of type | ||||||||||||||||||
| * `PAYLOAD_TYPE_1_DOCUMENT_SEQUENCE`. | ||||||||||||||||||
|
|
@@ -235,6 +275,21 @@ protected void encodeMessageBody(final ByteBufferBsonOutput bsonOutput, final Op | |||||||||||||||||
| this.firstDocumentPosition = useOpMsg() ? writeOpMsg(bsonOutput, operationContext) : writeOpQuery(bsonOutput); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Encodes the message, attaching the OP_MSG telemetry section with {@code commandSpan}'s trace context when | ||||||||||||||||||
| * the gating conditions hold (see {@link #writeTelemetryContextSection}). The 2-arg {@link | ||||||||||||||||||
| * #encode(ByteBufferBsonOutput, OperationContext)} never attaches the section. | ||||||||||||||||||
| */ | ||||||||||||||||||
| public void encode(final ByteBufferBsonOutput bsonOutput, final OperationContext operationContext, | ||||||||||||||||||
| @Nullable final Span commandSpan) { | ||||||||||||||||||
| this.commandSpanForEncoding = commandSpan; | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: This feels like an antipattern - global state that is safe with the try finally but its a bit hidden. Would it be clearer if this was threaded into encode as a parameter instead? |
||||||||||||||||||
| try { | ||||||||||||||||||
| encode(bsonOutput, operationContext); | ||||||||||||||||||
| } finally { | ||||||||||||||||||
| this.commandSpanForEncoding = null; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| @SuppressWarnings("try") | ||||||||||||||||||
| private int writeOpMsg(final ByteBufferBsonOutput bsonOutput, final OperationContext operationContext) { | ||||||||||||||||||
| int messageStartPosition = bsonOutput.getPosition() - MESSAGE_PROLOGUE_LENGTH; | ||||||||||||||||||
|
|
@@ -277,11 +332,35 @@ private int writeOpMsg(final ByteBufferBsonOutput bsonOutput, final OperationCon | |||||||||||||||||
| fail(sequences.toString()); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| writeTelemetryContextSection(bsonOutput); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Write the flag bits | ||||||||||||||||||
| bsonOutput.writeInt32(flagPosition, getOpMsgFlagBits()); | ||||||||||||||||||
| return commandStartPosition; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| private void writeTelemetryContextSection(final ByteBufferBsonOutput bsonOutput) { | ||||||||||||||||||
| if (getSettings().getMaxWireVersion() < NINE_DOT_ZERO_WIRE_VERSION) { | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| Span commandSpan = this.commandSpanForEncoding; | ||||||||||||||||||
| if (commandSpan == null) { | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| String traceParent = commandSpan.context().traceParent(); | ||||||||||||||||||
| if (traceParent == null) { | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| bsonOutput.writeByte(PAYLOAD_TYPE_3_TELEMETRY); | ||||||||||||||||||
| // {otel: {traceparent: <value>}} | ||||||||||||||||||
| BsonBinaryWriter writer = new BsonBinaryWriter(bsonOutput); | ||||||||||||||||||
| writer.writeStartDocument(); | ||||||||||||||||||
| writer.writeStartDocument("otel"); | ||||||||||||||||||
| writer.writeString("traceparent", traceParent); | ||||||||||||||||||
| writer.writeEndDocument(); | ||||||||||||||||||
| writer.writeEndDocument(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| private int writeOpQuery(final ByteBufferBsonOutput bsonOutput) { | ||||||||||||||||||
| bsonOutput.writeInt32(0); | ||||||||||||||||||
| bsonOutput.writeCString(new MongoNamespace(getDatabase(), MongoNamespaceHelper.COMMAND_COLLECTION_NAME).getFullName()); | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -439,20 +439,19 @@ public boolean reauthenticationIsTriggered(@Nullable final Throwable t) { | |
| @Nullable | ||
| private <T> T sendAndReceiveInternal(final CommandMessage message, final Decoder<T> decoder, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got this feedback from Claude: It may be worth auto migrating |
||
| final OperationContext operationContext) { | ||
| CommandEventSender commandEventSender; | ||
| Span tracingSpan; | ||
| CommandEventSender commandEventSender = new NoOpCommandEventSender(); | ||
| Span tracingSpan = null; | ||
| try (ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(this)) { | ||
| message.encode(bsonOutput, operationContext); | ||
| tracingSpan = operationContext | ||
| .getTracingManager() | ||
| .createTracingSpan(message, | ||
| operationContext, | ||
| () -> message.getCommandDocument(bsonOutput), | ||
| cmdName -> SECURITY_SENSITIVE_COMMANDS.contains(cmdName) | ||
| || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(cmdName), | ||
| () -> getDescription().getServerAddress(), | ||
| () -> getDescription().getConnectionId() | ||
| ); | ||
| message.encode(bsonOutput, operationContext, tracingSpan); | ||
| boolean isLoggingCommandNeeded = isLoggingCommandNeeded(); | ||
| boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled(); | ||
|
|
||
|
|
@@ -467,27 +466,22 @@ private <T> T sendAndReceiveInternal(final CommandMessage message, final Decoder | |
| operationContext, message, commandDocument, | ||
| COMMAND_PROTOCOL_LOGGER, loggerSettings); | ||
| commandEventSender.sendStartedEvent(); | ||
| } else { | ||
| commandEventSender = new NoOpCommandEventSender(); | ||
| } | ||
| if (isTracingCommandPayloadNeeded) { | ||
| tracingSpan.setQueryText(commandDocument); | ||
| } | ||
| if (tracingSpan != null) { | ||
| tracingSpan.openScope(); | ||
| } | ||
|
|
||
| try { | ||
| sendCommandMessage(message, bsonOutput, operationContext); | ||
| } catch (Exception e) { | ||
| if (tracingSpan != null) { | ||
| tracingSpan.error(e); | ||
| tracingSpan.closeScope(); | ||
| tracingSpan.end(); | ||
| } | ||
| commandEventSender.sendFailedEvent(e); | ||
| throw e; | ||
| sendCommandMessage(message, bsonOutput, operationContext); | ||
| } catch (Throwable t) { | ||
| if (tracingSpan != null) { | ||
| tracingSpan.error(t); | ||
| tracingSpan.closeScope(); | ||
| tracingSpan.end(); | ||
| } | ||
| commandEventSender.sendFailedEvent(t); | ||
| throw t; | ||
| } | ||
|
|
||
| if (message.isResponseExpected()) { | ||
|
|
@@ -528,7 +522,7 @@ private void sendCommandMessage(final CommandMessage message, final ByteBufferBs | |
| final OperationContext operationContext) { | ||
|
|
||
| Compressor localSendCompressor = sendCompressor; | ||
| if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandDocument(bsonOutput).getFirstKey())) { | ||
| if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandName())) { | ||
| trySendMessage(message, bsonOutput, operationContext); | ||
| } else { | ||
| ByteBufferBsonOutput compressedBsonOutput; | ||
|
|
@@ -617,19 +611,18 @@ private <T> void sendAndReceiveAsyncInternal(final CommandMessage message, final | |
|
|
||
| Span tracingSpan = null; | ||
| try { | ||
| message.encode(bsonOutput, operationContext); | ||
|
|
||
| tracingSpan = operationContext | ||
| .getTracingManager() | ||
| .createTracingSpan(message, | ||
| operationContext, | ||
| () -> message.getCommandDocument(bsonOutput), | ||
| cmdName -> SECURITY_SENSITIVE_COMMANDS.contains(cmdName) | ||
| || SECURITY_SENSITIVE_HELLO_COMMANDS.contains(cmdName), | ||
| () -> getDescription().getServerAddress(), | ||
| () -> getDescription().getConnectionId() | ||
| ); | ||
|
|
||
| message.encode(bsonOutput, operationContext, tracingSpan); | ||
|
|
||
| CommandEventSender commandEventSender; | ||
| boolean isLoggingCommandNeeded = isLoggingCommandNeeded(); | ||
| boolean isTracingCommandPayloadNeeded = tracingSpan != null && operationContext.getTracingManager().isCommandPayloadEnabled(); | ||
|
|
@@ -670,7 +663,7 @@ private <T> void sendAndReceiveAsyncInternal(final CommandMessage message, final | |
|
|
||
| commandEventSender.sendStartedEvent(); | ||
| Compressor localSendCompressor = sendCompressor; | ||
| if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandDocument(bsonOutput).getFirstKey())) { | ||
| if (localSendCompressor == null || SECURITY_SENSITIVE_COMMANDS.contains(message.getCommandName())) { | ||
| sendCommandMessageAsync(message.getId(), decoder, operationContext, tracingCallback, bsonOutput, commandEventSender, | ||
| message.isResponseExpected()); | ||
| } else { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth caching this in the constructor? I don't think theres a path where we don't call it?