diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java index 680220b5e3e..6170e1925d2 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/JsonSerializer.java @@ -122,6 +122,22 @@ public void writeString( generator.writeString(string); } + @Override + public void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws IOException { + generator.writeArrayFieldStart(field.getJsonName()); + for (byte[] value : utf8Bytes) { + // Marshalers encoded String into UTF-8 bytes to optimize for binary serialization where + // we are able to avoid the encoding process happening twice, one for size computation and one + // for actual writing. JsonGenerator actually has a writeUTF8String that would be able to + // accept + // this, but it only works when writing to an OutputStream, but not to a String like we do for + // writing to logs. It's wasteful to take a String, convert it to bytes, and convert back to + // the same String but we can see if this can be improved in the future. + generator.writeString(new String(value, StandardCharsets.UTF_8)); + } + generator.writeEndArray(); + } + @Override public void writeBytes(ProtoFieldInfo field, byte[] value) throws IOException { generator.writeBinaryField(field.getJsonName(), value); diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/ProtoSerializer.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/ProtoSerializer.java index 62f4a175982..694cec8b2b9 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/ProtoSerializer.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/ProtoSerializer.java @@ -163,6 +163,13 @@ public void writeString( StatelessMarshalerUtil.writeUtf8(output, string, utf8Length, context); } + @Override + public void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws IOException { + for (byte[] value : utf8Bytes) { + writeString(field, value); + } + } + @Override public void writeBytes(ProtoFieldInfo field, byte[] value) throws IOException { output.writeUInt32NoTag(field.getTag()); diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java index e7970d57491..069fa3a6b58 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/Serializer.java @@ -220,6 +220,18 @@ public void serializeString(ProtoFieldInfo field, byte[] utf8Bytes) throws IOExc writeString(field, utf8Bytes); } + /** + * Serializes a protobuf {@code repeated string} field. {@code utf8Bytes} is the UTF8 encoded + * bytes of the strings to serialize. + */ + @SuppressWarnings("AvoidObjectArrays") + public void serializeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) throws IOException { + if (utf8Bytes.length == 0) { + return; + } + writeRepeatedString(field, utf8Bytes); + } + /** * Serializes a protobuf {@code string} field. {@code string} is the value to be serialized and * {@code utf8Length} is the length of the string after it is encoded in UTF8. This method reads @@ -246,6 +258,11 @@ public abstract void writeString( ProtoFieldInfo field, String string, int utf8Length, MarshalerContext context) throws IOException; + /** Writes a protobuf {@code repeated string} field, even if it matches the default value. */ + @SuppressWarnings("AvoidObjectArrays") + public abstract void writeRepeatedString(ProtoFieldInfo field, byte[][] utf8Bytes) + throws IOException; + /** Serializes a protobuf {@code bytes} field. */ public void serializeBytes(ProtoFieldInfo field, byte[] value) throws IOException { if (value.length == 0) { diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityRefMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityRefMarshaler.java index 0c4453f9f36..b76b6ea34e4 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityRefMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityRefMarshaler.java @@ -33,12 +33,10 @@ protected void writeTo(Serializer output) throws IOException { output.writeString(ResourceEntityRefExperimental.SCHEMA_URL, schemaUrlUtf8); } output.writeString(ResourceEntityRefExperimental.TYPE, typeUtf8); - for (byte[] keyUtf8 : identityAttributeKeysUtf8) { - output.serializeString(ResourceEntityRefExperimental.IDENTITY_ATTRIBUTES, keyUtf8); - } - for (byte[] keyUtf8 : descriptiveAttributeKeysUtf8) { - output.serializeString(ResourceEntityRefExperimental.DESCRIPTION_ATTRIBUTES, keyUtf8); - } + output.writeRepeatedString( + ResourceEntityRefExperimental.IDENTITY_ATTRIBUTES, identityAttributeKeysUtf8); + output.writeRepeatedString( + ResourceEntityRefExperimental.DESCRIPTION_ATTRIBUTES, descriptiveAttributeKeysUtf8); } public static ResourceEntityRefMarshaler createForEntity(Entity e) { @@ -81,6 +79,7 @@ private static int calculateSize( size += MarshalerUtil.sizeBytes(ResourceEntityRefExperimental.SCHEMA_URL, schemaUrlUtf8); } size += MarshalerUtil.sizeBytes(ResourceEntityRefExperimental.TYPE, typeUtf8); + // TODO - we need repeated string support. for (byte[] keyUtf8 : identityAttributeKeysUtf8) { size += MarshalerUtil.sizeBytes(ResourceEntityRefExperimental.IDENTITY_ATTRIBUTES, keyUtf8); } diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityTest.java new file mode 100644 index 00000000000..afd312c7c9d --- /dev/null +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/ResourceEntityTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.resources.Entity; +import io.opentelemetry.sdk.resources.Resource; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +public class ResourceEntityTest { + @Test + void toJsonResourceWithEntity() throws Exception { + Resource resource = + Resource.builder() + .add( + Entity.builder() + .setSchemaUrl("http://example.com/1.0") + .setEntityType("test") + .withIdentifying(attr -> attr.put("test.id", 1)) + .withDescriptive(attr -> attr.put("test.name", "one")) + .build()) + .build(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + ResourceMarshaler.create(resource).writeJsonTo(out); + } finally { + out.close(); + } + + String json = new String(out.toByteArray(), StandardCharsets.UTF_8); + assertThat(json) + .isEqualTo( + "{\"attributes\":[{\"key\":\"test.id\",\"value\":{\"intValue\":\"1\"}},{\"key\":\"test.name\",\"value\":{\"stringValue\":\"one\"}}]," + + "\"entityRefs\":[{\"schemaUrl\":\"http://example.com/1.0\",\"type\":\"test\",\"idAttrKeys\":[\"test.id\"],\"descrAttrKeys\":[\"test.name\"]}]}"); + } +}