diff --git a/communication/src/main/java/datadog/communication/serialization/GrowableBuffer.java b/communication/src/main/java/datadog/communication/serialization/GrowableBuffer.java index 8f7a471e2d..81b55aba7d 100644 --- a/communication/src/main/java/datadog/communication/serialization/GrowableBuffer.java +++ b/communication/src/main/java/datadog/communication/serialization/GrowableBuffer.java @@ -17,11 +17,18 @@ public GrowableBuffer(int initialCapacity) { this.buffer = ByteBuffer.allocate(initialCapacity); } + /** Flips the buffer and returns a new slice which shares the buffered content. */ public ByteBuffer slice() { buffer.flip(); return buffer.slice(); } + /** Flips the buffer and returns the buffered content. */ + public ByteBuffer flip() { + buffer.flip(); + return buffer; + } + public int messageCount() { return messageCount; } diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpCommonProto.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpCommonProto.java index d2836d5398..74c81614f6 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpCommonProto.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpCommonProto.java @@ -3,10 +3,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import datadog.communication.serialization.GenerationalUtf8Cache; +import datadog.communication.serialization.GrowableBuffer; import datadog.communication.serialization.SimpleUtf8Cache; import datadog.communication.serialization.StreamingBuffer; import datadog.trace.api.Config; import datadog.trace.bootstrap.otel.common.OtelInstrumentationScope; +import java.nio.ByteBuffer; import java.util.List; /** @@ -43,6 +45,14 @@ public static int sizeVarInt(long value) { return 1 + (63 - Long.numberOfLeadingZeros(value)) / 7; } + public static void writeVarInt(ByteBuffer buf, int value) { + for (int i = 1, len = sizeVarInt(value); i < len; i++) { + buf.put((byte) ((value & 0x7f) | 0x80)); + value >>>= 7; + } + buf.put((byte) value); + } + public static void writeVarInt(StreamingBuffer buf, int value) { for (int i = 1, len = sizeVarInt(value); i < len; i++) { buf.put((byte) ((value & 0x7f) | 0x80)); @@ -96,10 +106,28 @@ public static void writeString(StreamingBuffer buf, String value) { writeString(buf, value.getBytes(UTF_8)); } + public static void writeTag(ByteBuffer buf, int fieldNum, int wireType) { + writeVarInt(buf, fieldNum << 3 | wireType); + } + public static void writeTag(StreamingBuffer buf, int fieldNum, int wireType) { writeVarInt(buf, fieldNum << 3 | wireType); } + public static byte[] recordMessage(GrowableBuffer buf, int fieldNum) { + try { + ByteBuffer data = buf.flip(); + int dataSize = data.remaining(); + ByteBuffer message = ByteBuffer.allocate(1 + sizeVarInt(dataSize) + dataSize); + writeTag(message, fieldNum, LEN_WIRE_TYPE); + writeVarInt(message, dataSize); + message.put(data); + return message.array(); + } finally { + buf.reset(); + } + } + public static void writeInstrumentationScope( StreamingBuffer buf, OtelInstrumentationScope scope) { byte[] scopeNameUtf8 = scope.getName().getUtf8Bytes(); diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProto.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProto.java new file mode 100644 index 0000000000..8c99208394 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProto.java @@ -0,0 +1,56 @@ +package datadog.trace.bootstrap.otel.common.export; + +import static datadog.trace.bootstrap.otel.common.export.OtlpAttributeVisitor.STRING; +import static datadog.trace.bootstrap.otel.common.export.OtlpCommonProto.LEN_WIRE_TYPE; +import static datadog.trace.bootstrap.otel.common.export.OtlpCommonProto.recordMessage; +import static datadog.trace.bootstrap.otel.common.export.OtlpCommonProto.writeAttribute; +import static datadog.trace.bootstrap.otel.common.export.OtlpCommonProto.writeTag; + +import datadog.communication.serialization.GrowableBuffer; +import datadog.communication.serialization.StreamingBuffer; +import datadog.trace.api.Config; + +/** Provides a canned message for OpenTelemetry's "resource.proto" wire protocol. */ +public final class OtlpResourceProto { + private static final byte[] RESOURCE_MESSAGE = buildResourceMessage(Config.get()); + + /** Writes the resource message in protobuf format to the given buffer. */ + public static void writeResourceMessage(StreamingBuffer buf) { + buf.put(RESOURCE_MESSAGE); + } + + static byte[] buildResourceMessage(Config config) { + GrowableBuffer buf = new GrowableBuffer(512); + + String serviceName = config.getServiceName(); + String env = config.getEnv(); + String version = config.getVersion(); + + writeResourceAttribute(buf, "service.name", serviceName); + if (!env.isEmpty()) { + writeResourceAttribute(buf, "deployment.environment.name", env); + } + if (!version.isEmpty()) { + writeResourceAttribute(buf, "service.version", version); + } + + config + .getGlobalTags() + .forEach( + (key, value) -> { + // ignore datadog tags that we map above + if (!"service".equalsIgnoreCase(key) + && !"env".equalsIgnoreCase(key) + && !"version".equalsIgnoreCase(key)) { + writeResourceAttribute(buf, key, value); + } + }); + + return recordMessage(buf, 1); + } + + private static void writeResourceAttribute(StreamingBuffer buf, String key, String value) { + writeTag(buf, 1, LEN_WIRE_TYPE); + writeAttribute(buf, STRING, key, value); + } +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/test/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProtoTest.java b/dd-java-agent/agent-otel/otel-bootstrap/src/test/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProtoTest.java new file mode 100644 index 0000000000..49546d3f1a --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/test/java/datadog/trace/bootstrap/otel/common/export/OtlpResourceProtoTest.java @@ -0,0 +1,194 @@ +package datadog.trace.bootstrap.otel.common.export; + +import static datadog.trace.api.config.GeneralConfig.ENV; +import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; +import static datadog.trace.api.config.GeneralConfig.TAGS; +import static datadog.trace.api.config.GeneralConfig.VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.WireFormat; +import datadog.trace.api.Config; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for {@link OtlpResourceProto#buildResourceMessage}. + * + *
Each test creates a {@link Config} from a {@link Properties} instance, calls {@link + * OtlpResourceProto#buildResourceMessage}, then extracts the byte array and verifies its content + * against the OpenTelemetry protobuf encoding defined in {@code + * opentelemetry/proto/resource/v1/resource.proto}. + * + *
Relevant proto field numbers: + * + *
+ * Resource { repeated KeyValue attributes = 1; }
+ * KeyValue { string key = 1; AnyValue value = 2; }
+ * AnyValue { string string_value = 1; }
+ *
+ */
+class OtlpResourceProtoTest {
+
+ // ── test data ─────────────────────────────────────────────────────────────
+
+ private static Properties props(String... keyValues) {
+ Properties props = new Properties();
+ for (int i = 0; i < keyValues.length; i += 2) {
+ props.setProperty(keyValues[i], keyValues[i + 1]);
+ }
+ return props;
+ }
+
+ private static Map{@code buildResourceMessage} returns a length-prefixed message with an outer tag (field 1,
+ * LEN wire type) followed by the Resource body size and body. Read the outer tag, then iterate
+ * over all {@code Resource.attributes} (field 1, LEN wire type). Each attribute is a {@code
+ * KeyValue} whose {@code value} is an {@code AnyValue} containing a {@code string_value}.
+ */
+ private static Map