diff --git a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java index 7743c315c50..7d012aa14ca 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java @@ -26,6 +26,11 @@ public interface AttributeKey { /** Returns the type of attribute for this key. Useful for building switch statements. */ AttributeType getType(); + // TODO (jack-berg): uncomment when extended attributes are promoted from incubator to API + // default ExtendedAttributeKey asExtendedAttributeKey() { + // return InternalAttributeKeyImpl.toExtendedAttributeKey(this); + // } + /** Returns a new AttributeKey for String valued attributes. */ static AttributeKey stringKey(String key) { return InternalAttributeKeyImpl.create(key, AttributeType.STRING); diff --git a/api/incubator/README.md b/api/incubator/README.md index e0b0ec7a080..04a271255be 100644 --- a/api/incubator/README.md +++ b/api/incubator/README.md @@ -7,6 +7,7 @@ Experimental APIs, including Event API, extended Log Bridge APIs, extended Metri Features: * Check if logger is enabled before emitting logs to avoid unnecessary computation +* Add extended attributes to log records to encode complex data structures See [ExtendedLogsBridgeApiUsageTest](./src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java). diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java new file mode 100644 index 00000000000..a864a1d9585 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributes.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.internal.ImmutableKeyValuePairs; +import java.util.ArrayList; +import java.util.Comparator; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ArrayBackedExtendedAttributes + extends ImmutableKeyValuePairs, Object> implements ExtendedAttributes { + + // We only compare the key name, not type, when constructing, to allow deduping keys with the + // same name but different type. + private static final Comparator> KEY_COMPARATOR_FOR_CONSTRUCTION = + Comparator.comparing(ExtendedAttributeKey::getKey); + + static final ExtendedAttributes EMPTY = ExtendedAttributes.builder().build(); + + @Nullable private Attributes attributes; + + private ArrayBackedExtendedAttributes( + Object[] data, Comparator> keyComparator) { + super(data, keyComparator); + } + + /** + * Only use this constructor if you can guarantee that the data has been de-duped, sorted by key + * and contains no null values or null/empty keys. + * + * @param data the raw data + */ + ArrayBackedExtendedAttributes(Object[] data) { + super(data); + } + + @Override + public ExtendedAttributesBuilder toBuilder() { + return new ArrayBackedExtendedAttributesBuilder(new ArrayList<>(data())); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(ExtendedAttributeKey key) { + return (T) super.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public Attributes asAttributes() { + if (attributes == null) { + AttributesBuilder builder = Attributes.builder(); + forEach( + (extendedAttributeKey, value) -> { + AttributeKey attributeKey = + (AttributeKey) extendedAttributeKey.asAttributeKey(); + if (attributeKey != null) { + builder.put(attributeKey, value); + } + }); + attributes = builder.build(); + } + return attributes; + } + + static ExtendedAttributes sortAndFilterToAttributes(Object... data) { + // null out any empty keys or keys with null values + // so they will then be removed by the sortAndFilter method. + for (int i = 0; i < data.length; i += 2) { + ExtendedAttributeKey key = (ExtendedAttributeKey) data[i]; + if (key != null && key.getKey().isEmpty()) { + data[i] = null; + } + } + return new ArrayBackedExtendedAttributes(data, KEY_COMPARATOR_FOR_CONSTRUCTION); + } +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java new file mode 100644 index 00000000000..891707c61af --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ArrayBackedExtendedAttributesBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +class ArrayBackedExtendedAttributesBuilder implements ExtendedAttributesBuilder { + private final List data; + + ArrayBackedExtendedAttributesBuilder() { + data = new ArrayList<>(); + } + + ArrayBackedExtendedAttributesBuilder(List data) { + this.data = data; + } + + @Override + public ExtendedAttributes build() { + // If only one key-value pair AND the entry hasn't been set to null (by #remove(AttributeKey) + // or #removeIf(Predicate>)), then we can bypass sorting and filtering + if (data.size() == 2 && data.get(0) != null) { + return new ArrayBackedExtendedAttributes(data.toArray()); + } + return ArrayBackedExtendedAttributes.sortAndFilterToAttributes(data.toArray()); + } + + @Override + public ExtendedAttributesBuilder put(ExtendedAttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + data.add(key); + data.add(value); + return this; + } + + @Override + public ExtendedAttributesBuilder removeIf(Predicate> predicate) { + if (predicate == null) { + return this; + } + for (int i = 0; i < data.size() - 1; i += 2) { + Object entry = data.get(i); + if (entry instanceof ExtendedAttributeKey + && predicate.test((ExtendedAttributeKey) entry)) { + // null items are filtered out in ArrayBackedAttributes + data.set(i, null); + data.set(i + 1, null); + } + } + return this; + } + + static List toList(double... values) { + Double[] boxed = new Double[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } + + static List toList(long... values) { + Long[] boxed = new Long[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } + + static List toList(boolean... values) { + Boolean[] boxed = new Boolean[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java new file mode 100644 index 00000000000..13357a31a56 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKey.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; +import java.util.List; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * This interface provides a handle for setting the values of {@link ExtendedAttributes}. The type + * of value that can be set with an implementation of this key is denoted by the type parameter. + * + *

Implementations MUST be immutable, as these are used as the keys to Maps. + * + *

The allowed {@link #getType()}s is a superset of those allowed in {@link AttributeKey}. + * + *

Convenience methods are provided for translating to / from {@link AttributeKey}: + * + *

    + *
  • {@link #asAttributeKey()} converts from {@link ExtendedAttributeKey} to {@link + * AttributeKey} + *
  • {@link #fromAttributeKey(AttributeKey)} converts from {@link AttributeKey} to {@link + * ExtendedAttributeKey} + *
+ * + * @param The type of value that can be set with the key. + */ +@Immutable +public interface ExtendedAttributeKey { + /** Returns the underlying String representation of the key. */ + String getKey(); + + /** Returns the type of attribute for this key. Useful for building switch statements. */ + ExtendedAttributeType getType(); + + /** + * Return the equivalent {@link AttributeKey}, or {@code null} if the {@link #getType()} has no + * equivalent {@link io.opentelemetry.api.common.AttributeType}. + */ + @Nullable + default AttributeKey asAttributeKey() { + return InternalExtendedAttributeKeyImpl.toAttributeKey(this); + } + + /** Return an ExtendedAttributeKey equivalent to the {@code attributeKey}. */ + // TODO (jack-berg): remove once AttributeKey.asExtendedAttributeKey is available + static ExtendedAttributeKey fromAttributeKey(AttributeKey attributeKey) { + return InternalExtendedAttributeKeyImpl.toExtendedAttributeKey(attributeKey); + } + + /** Returns a new ExtendedAttributeKey for String valued attributes. */ + static ExtendedAttributeKey stringKey(String key) { + return fromAttributeKey(AttributeKey.stringKey(key)); + } + + /** Returns a new ExtendedAttributeKey for Boolean valued attributes. */ + static ExtendedAttributeKey booleanKey(String key) { + return fromAttributeKey(AttributeKey.booleanKey(key)); + } + + /** Returns a new ExtendedAttributeKey for Long valued attributes. */ + static ExtendedAttributeKey longKey(String key) { + return fromAttributeKey(AttributeKey.longKey(key)); + } + + /** Returns a new ExtendedAttributeKey for Double valued attributes. */ + static ExtendedAttributeKey doubleKey(String key) { + return fromAttributeKey(AttributeKey.doubleKey(key)); + } + + /** Returns a new ExtendedAttributeKey for List<String> valued attributes. */ + static ExtendedAttributeKey> stringArrayKey(String key) { + return fromAttributeKey(AttributeKey.stringArrayKey(key)); + } + + /** Returns a new ExtendedAttributeKey for List<Boolean> valued attributes. */ + static ExtendedAttributeKey> booleanArrayKey(String key) { + return fromAttributeKey(AttributeKey.booleanArrayKey(key)); + } + + /** Returns a new ExtendedAttributeKey for List<Long> valued attributes. */ + static ExtendedAttributeKey> longArrayKey(String key) { + return fromAttributeKey(AttributeKey.longArrayKey(key)); + } + + /** Returns a new ExtendedAttributeKey for List<Double> valued attributes. */ + static ExtendedAttributeKey> doubleArrayKey(String key) { + return fromAttributeKey(AttributeKey.doubleArrayKey(key)); + } + + /** Returns a new ExtendedAttributeKey for Map valued attributes. */ + static ExtendedAttributeKey extendedAttributesKey(String key) { + return InternalExtendedAttributeKeyImpl.create(key, ExtendedAttributeType.EXTENDED_ATTRIBUTES); + } +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java new file mode 100644 index 00000000000..8d2c67181b6 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributeType.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +/** + * An enum that represents all the possible value types for an {@link ExtendedAttributeKey} and + * hence the types of values that are allowed for {@link ExtendedAttributes}. + * + *

This is a superset of {@link io.opentelemetry.api.common.AttributeType}, + */ +public enum ExtendedAttributeType { + // Types copied AttributeType + STRING, + BOOLEAN, + LONG, + DOUBLE, + STRING_ARRAY, + BOOLEAN_ARRAY, + LONG_ARRAY, + DOUBLE_ARRAY, + // Extended types unique to ExtendedAttributes + EXTENDED_ATTRIBUTES; +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java new file mode 100644 index 00000000000..0fc88a2ea49 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributes.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An immutable container for extended attributes. + * + *

"extended" refers an extended set of allowed value types compared to standard {@link + * Attributes}. Notably, {@link ExtendedAttributes} values can be of type {@link + * ExtendedAttributeType#EXTENDED_ATTRIBUTES}, allowing nested {@link ExtendedAttributes} of + * arbitrary depth. + * + *

Where standard {@link Attributes} are accepted everyone that OpenTelemetry represents key / + * value pairs, {@link ExtendedAttributes} are only accepted in select places, such as log records + * (e.g. {@link ExtendedLogRecordBuilder#setAttribute(ExtendedAttributeKey, Object)}). + * + *

The keys are {@link ExtendedAttributeKey}s and the values are Object instances that match the + * type of the provided key. + * + *

Null keys will be silently dropped. + * + *

Note: The behavior of null-valued attributes is undefined, and hence strongly discouraged. + * + *

Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. + * + *

For this reason, it is strongly suggested that you use the implementation that is provided + * here via the factory methods and the {@link ExtendedAttributesBuilder}. + * + *

Convenience methods are provided for translating to / from {@link Attributes}: + * + *

    + *
  • {@link #asAttributes()} converts from {@link ExtendedAttributes} to {@link Attributes} + *
  • {@link ExtendedAttributesBuilder#putAll(Attributes)} converts from {@link Attributes} to + * {@link ExtendedAttributes} + *
  • {@link #get(AttributeKey)} supports reading values using standard {@link AttributeKey} + *
+ */ +@Immutable +public interface ExtendedAttributes { + + /** Returns the value for the given {@link AttributeKey}, or {@code null} if not found. */ + @Nullable + default T get(AttributeKey key) { + if (key == null) { + return null; + } + return get(ExtendedAttributeKey.fromAttributeKey(key)); + } + + /** Returns the value for the given {@link ExtendedAttributeKey}, or {@code null} if not found. */ + @Nullable + T get(ExtendedAttributeKey key); + + /** Iterates over all the key-value pairs of attributes contained by this instance. */ + void forEach(BiConsumer, ? super Object> consumer); + + /** The number of attributes contained in this. */ + int size(); + + /** Whether there are any attributes contained in this. */ + boolean isEmpty(); + + /** Returns a read-only view of this {@link ExtendedAttributes} as a {@link Map}. */ + Map, Object> asMap(); + + /** + * Return a view of this extended attributes with entries limited to those representable as + * standard attributes. + */ + Attributes asAttributes(); + + /** Returns a {@link ExtendedAttributes} instance with no attributes. */ + static ExtendedAttributes empty() { + return ArrayBackedExtendedAttributes.EMPTY; + } + + /** + * Returns a new {@link ExtendedAttributesBuilder} instance for creating arbitrary {@link + * ExtendedAttributes}. + */ + static ExtendedAttributesBuilder builder() { + return new ArrayBackedExtendedAttributesBuilder(); + } + + /** + * Returns a new {@link ExtendedAttributesBuilder} instance populated with the data of this {@link + * ExtendedAttributes}. + */ + ExtendedAttributesBuilder toBuilder(); +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java new file mode 100644 index 00000000000..1e0de3b4c38 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/common/ExtendedAttributesBuilder.java @@ -0,0 +1,233 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import static io.opentelemetry.api.incubator.common.ArrayBackedExtendedAttributesBuilder.toList; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.booleanKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.doubleKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.longKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringArrayKey; +import static io.opentelemetry.api.incubator.common.ExtendedAttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +/** A builder of {@link ExtendedAttributes} supporting an arbitrary number of key-value pairs. */ +public interface ExtendedAttributesBuilder { + /** Create the {@link ExtendedAttributes} from this. */ + ExtendedAttributes build(); + + /** Puts a {@link AttributeKey} with associated value into this. */ + default ExtendedAttributesBuilder put(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + return put(ExtendedAttributeKey.fromAttributeKey(key), value); + } + + /** Puts a {@link ExtendedAttributeKey} with associated value into this. */ + ExtendedAttributesBuilder put(ExtendedAttributeKey key, T value); + + /** + * Puts a String attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, String value) { + return put(stringKey(key), value); + } + + /** + * Puts a long attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, long value) { + return put(longKey(key), value); + } + + /** + * Puts a double attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, double value) { + return put(doubleKey(key), value); + } + + /** + * Puts a boolean attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, boolean value) { + return put(booleanKey(key), value); + } + + /** + * Puts a {@link ExtendedAttributes} attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, ExtendedAttributes value) { + return put(ExtendedAttributeKey.extendedAttributesKey(key), value); + } + + /** + * Puts a String array attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, String... value) { + if (value == null) { + return this; + } + return put(stringArrayKey(key), Arrays.asList(value)); + } + + /** + * Puts a List attribute into this. + * + * @return this Builder + */ + @SuppressWarnings("unchecked") + default ExtendedAttributesBuilder put(AttributeKey> key, T... value) { + if (value == null) { + return this; + } + return put(key, Arrays.asList(value)); + } + + /** + * Puts a Long array attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, long... value) { + if (value == null) { + return this; + } + return put(longArrayKey(key), toList(value)); + } + + /** + * Puts a Double array attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, double... value) { + if (value == null) { + return this; + } + return put(doubleArrayKey(key), toList(value)); + } + + /** + * Puts a Boolean array attribute into this. + * + *

Note: It is strongly recommended to use {@link #put(ExtendedAttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @return this Builder + */ + default ExtendedAttributesBuilder put(String key, boolean... value) { + if (value == null) { + return this; + } + return put(booleanArrayKey(key), toList(value)); + } + + /** + * Puts all the provided attributes into this Builder. + * + * @return this Builder + */ + @SuppressWarnings({"unchecked"}) + default ExtendedAttributesBuilder putAll(Attributes attributes) { + if (attributes == null) { + return this; + } + attributes.forEach((key, value) -> put((AttributeKey) key, value)); + return this; + } + + /** + * Puts all the provided attributes into this Builder. + * + * @return this Builder + */ + @SuppressWarnings({"unchecked"}) + default ExtendedAttributesBuilder putAll(ExtendedAttributes attributes) { + if (attributes == null) { + return this; + } + attributes.forEach((key, value) -> put((ExtendedAttributeKey) key, value)); + return this; + } + + /** + * Remove all attributes where {@link AttributeKey#getKey()} and {@link AttributeKey#getType()} + * match the {@code key}. + * + * @return this Builder + */ + default ExtendedAttributesBuilder remove(AttributeKey key) { + return remove(ExtendedAttributeKey.fromAttributeKey(key)); + } + + /** + * Remove all attributes where {@link ExtendedAttributeKey#getKey()} and {@link + * ExtendedAttributeKey#getType()} match the {@code key}. + * + * @return this Builder + */ + default ExtendedAttributesBuilder remove(ExtendedAttributeKey key) { + if (key == null || key.getKey().isEmpty()) { + return this; + } + return removeIf( + entryKey -> + key.getKey().equals(entryKey.getKey()) && key.getType().equals(entryKey.getType())); + } + + /** + * Remove all attributes that satisfy the given predicate. Errors or runtime exceptions thrown by + * the predicate are relayed to the caller. + * + * @return this Builder + */ + ExtendedAttributesBuilder removeIf(Predicate> filter); +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java new file mode 100644 index 00000000000..e07f72f0121 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/internal/InternalExtendedAttributeKeyImpl.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributeType; +import io.opentelemetry.api.internal.InternalAttributeKeyImpl; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InternalExtendedAttributeKeyImpl implements ExtendedAttributeKey { + + private final ExtendedAttributeType type; + private final String key; + private final int hashCode; + + @Nullable private byte[] keyUtf8; + @Nullable private AttributeKey attributeKey; + + private InternalExtendedAttributeKeyImpl(ExtendedAttributeType type, String key) { + if (type == null) { + throw new NullPointerException("Null type"); + } + this.type = type; + if (key == null) { + throw new NullPointerException("Null key"); + } + this.key = key; + this.hashCode = buildHashCode(type, key); + } + + public static ExtendedAttributeKey create( + @Nullable String key, ExtendedAttributeType type) { + return new InternalExtendedAttributeKeyImpl<>(type, key != null ? key : ""); + } + + @Override + public ExtendedAttributeType getType() { + return type; + } + + @Override + public String getKey() { + return key; + } + + @Nullable + @Override + public AttributeKey asAttributeKey() { + if (attributeKey == null) { + attributeKey = toAttributeKey(this); + } + return attributeKey; + } + + /** Returns the key, encoded as UTF-8 bytes. */ + public byte[] getKeyUtf8() { + byte[] keyUtf8 = this.keyUtf8; + if (keyUtf8 == null) { + keyUtf8 = key.getBytes(StandardCharsets.UTF_8); + this.keyUtf8 = keyUtf8; + } + return keyUtf8; + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (o instanceof InternalExtendedAttributeKeyImpl) { + InternalExtendedAttributeKeyImpl that = (InternalExtendedAttributeKeyImpl) o; + return this.type.equals(that.getType()) && this.key.equals(that.getKey()); + } + return false; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return key; + } + + // this method exists to make EqualsVerifier happy + @SuppressWarnings("unused") + private int buildHashCode() { + return buildHashCode(type, key); + } + + private static int buildHashCode(ExtendedAttributeType type, String key) { + int result = 1; + result *= 1000003; + result ^= type.hashCode(); + result *= 1000003; + result ^= key.hashCode(); + return result; + } + + /** + * Return the equivalent {@link AttributeKey} for the {@link ExtendedAttributeKey}, or {@code + * null} if the {@link #getType()} has no equivalent {@link + * io.opentelemetry.api.common.AttributeType}. + */ + @Nullable + public static AttributeKey toAttributeKey(ExtendedAttributeKey extendedAttributeKey) { + switch (extendedAttributeKey.getType()) { + case STRING: + return InternalAttributeKeyImpl.create(extendedAttributeKey.getKey(), AttributeType.STRING); + case BOOLEAN: + return InternalAttributeKeyImpl.create( + extendedAttributeKey.getKey(), AttributeType.BOOLEAN); + case LONG: + return InternalAttributeKeyImpl.create(extendedAttributeKey.getKey(), AttributeType.LONG); + case DOUBLE: + return InternalAttributeKeyImpl.create(extendedAttributeKey.getKey(), AttributeType.DOUBLE); + case STRING_ARRAY: + return InternalAttributeKeyImpl.create( + extendedAttributeKey.getKey(), AttributeType.STRING_ARRAY); + case BOOLEAN_ARRAY: + return InternalAttributeKeyImpl.create( + extendedAttributeKey.getKey(), AttributeType.BOOLEAN_ARRAY); + case LONG_ARRAY: + return InternalAttributeKeyImpl.create( + extendedAttributeKey.getKey(), AttributeType.LONG_ARRAY); + case DOUBLE_ARRAY: + return InternalAttributeKeyImpl.create( + extendedAttributeKey.getKey(), AttributeType.DOUBLE_ARRAY); + case EXTENDED_ATTRIBUTES: + return null; + } + throw new IllegalArgumentException( + "Unrecognized extendedAttributeKey type: " + extendedAttributeKey.getType()); + } + + /** Return the equivalent {@link ExtendedAttributeKey} for the {@link AttributeKey}. */ + public static ExtendedAttributeKey toExtendedAttributeKey(AttributeKey attributeKey) { + switch (attributeKey.getType()) { + case STRING: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.STRING); + case BOOLEAN: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.BOOLEAN); + case LONG: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.LONG); + case DOUBLE: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.DOUBLE); + case STRING_ARRAY: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.STRING_ARRAY); + case BOOLEAN_ARRAY: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.BOOLEAN_ARRAY); + case LONG_ARRAY: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.LONG_ARRAY); + case DOUBLE_ARRAY: + return InternalExtendedAttributeKeyImpl.create( + attributeKey.getKey(), ExtendedAttributeType.DOUBLE_ARRAY); + } + throw new IllegalArgumentException("Unrecognized attributeKey type: " + attributeKey.getType()); + } +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedDefaultLogger.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedDefaultLogger.java index d9a95a6d2d4..f0f167bac09 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedDefaultLogger.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedDefaultLogger.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Value; -import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; @@ -46,52 +46,57 @@ public ExtendedLogRecordBuilder setException(Throwable throwable) { } @Override - public LogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + public ExtendedLogRecordBuilder setAttribute(ExtendedAttributeKey key, T value) { return this; } @Override - public LogRecordBuilder setTimestamp(Instant instant) { + public ExtendedLogRecordBuilder setAttribute(AttributeKey key, T value) { return this; } @Override - public LogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + public ExtendedLogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { return this; } @Override - public LogRecordBuilder setObservedTimestamp(Instant instant) { + public ExtendedLogRecordBuilder setTimestamp(Instant instant) { return this; } @Override - public LogRecordBuilder setContext(Context context) { + public ExtendedLogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { return this; } @Override - public LogRecordBuilder setSeverity(Severity severity) { + public ExtendedLogRecordBuilder setObservedTimestamp(Instant instant) { return this; } @Override - public LogRecordBuilder setSeverityText(String severityText) { + public ExtendedLogRecordBuilder setContext(Context context) { return this; } @Override - public LogRecordBuilder setBody(String body) { + public ExtendedLogRecordBuilder setSeverity(Severity severity) { return this; } @Override - public LogRecordBuilder setBody(Value body) { + public ExtendedLogRecordBuilder setSeverityText(String severityText) { return this; } @Override - public LogRecordBuilder setAttribute(AttributeKey key, T value) { + public ExtendedLogRecordBuilder setBody(String body) { + return this; + } + + @Override + public ExtendedLogRecordBuilder setBody(Value body) { return this; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedLogRecordBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedLogRecordBuilder.java index d6fde3699b1..9ca95f0b0cc 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedLogRecordBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedLogRecordBuilder.java @@ -5,13 +5,124 @@ package io.opentelemetry.api.incubator.logs; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import java.time.Instant; +import java.util.concurrent.TimeUnit; /** Extended {@link LogRecordBuilder} with experimental APIs. */ public interface ExtendedLogRecordBuilder extends LogRecordBuilder { // keep this class even if it is empty, since experimental methods may be added in the future. + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setTimestamp(long timestamp, TimeUnit unit); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setTimestamp(Instant instant); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setObservedTimestamp(Instant instant); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setContext(Context context); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setSeverity(Severity severity); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setSeverityText(String severityText); + + /** {@inheritDoc} */ + @Override + ExtendedLogRecordBuilder setBody(String body); + + /** {@inheritDoc} */ + @Override + default ExtendedLogRecordBuilder setBody(Value body) { + setBody(body.asString()); + return this; + } + + /** + * {@inheritDoc} + * + *

NOTE: all standard {@link AttributeKey}-value pairs can also be represented as {@link + * ExtendedAttributeKey}-value pairs, but not all {@link ExtendedAttributeKey}-value pairs can be + * represented as standard {@link AttributeKey}-value pairs. From the standpoint of the emitted + * log record, there is no difference between adding attributes using the standard or extended + * attribute APIs. + */ + @SuppressWarnings("unchecked") + @Override + default ExtendedLogRecordBuilder setAllAttributes(Attributes attributes) { + if (attributes == null || attributes.isEmpty()) { + return this; + } + attributes.forEach( + (attributeKey, value) -> setAttribute((AttributeKey) attributeKey, value)); + return this; + } + + /** + * Sets attributes. If the {@link LogRecordBuilder} previously contained a mapping for any of the + * keys, the old values are replaced by the specified values. + * + *

NOTE: all standard {@link AttributeKey}-value pairs can also be represented as {@link + * ExtendedAttributeKey}-value pairs, but not all {@link ExtendedAttributeKey}-value pairs can be + * represented as standard {@link AttributeKey}-value pairs. From the standpoint of the emitted + * log record, there is no difference between adding attributes using the standard or extended + * attribute APIs. + */ + @SuppressWarnings("unchecked") + default ExtendedLogRecordBuilder setAllAttributes(ExtendedAttributes attributes) { + if (attributes == null || attributes.isEmpty()) { + return this; + } + attributes.forEach( + (attributeKey, value) -> setAttribute((ExtendedAttributeKey) attributeKey, value)); + return this; + } + + /** + * {@inheritDoc} + * + *

NOTE: all standard {@link AttributeKey}-value pairs can also be represented as {@link + * ExtendedAttributeKey}-value pairs, but not all {@link ExtendedAttributeKey}-value pairs can be + * represented as standard {@link AttributeKey}-value pairs. From the standpoint of the emitted + * log record, there is no difference between adding attributes using the standard or extended + * attribute APIs. + */ + @Override + ExtendedLogRecordBuilder setAttribute(AttributeKey key, T value); + + /** + * Set an attribute. + * + *

NOTE: all standard {@link AttributeKey}-value pairs can also be represented as {@link + * ExtendedAttributeKey}-value pairs, but not all {@link ExtendedAttributeKey}-value pairs can be + * represented as standard {@link AttributeKey}-value pairs. From the standpoint of the emitted + * log record, there is no difference between adding attributes using the standard or extended + * attribute APIs. + */ + ExtendedLogRecordBuilder setAttribute(ExtendedAttributeKey key, T value); + /** * Sets the event name, which identifies the class / type of the Event. * diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java new file mode 100644 index 00000000000..d2f77625d1f --- /dev/null +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributeKeyTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class ExtendedAttributeKeyTest { + + @ParameterizedTest + @MethodSource("attributeKeyArgs") + void test( + ExtendedAttributeKey key, + String expectedKey, + ExtendedAttributeType expectedType, + @Nullable AttributeKey expectedAttributeKey) { + assertThat(key.getKey()).isEqualTo(expectedKey); + assertThat(key.getType()).isEqualTo(expectedType); + assertThat(key.asAttributeKey()).isEqualTo(expectedAttributeKey); + + if (expectedAttributeKey != null) { + ExtendedAttributeKey extendedAttributeKey = + ExtendedAttributeKey.fromAttributeKey(expectedAttributeKey); + assertThat(extendedAttributeKey).isEqualTo(key); + } + } + + private static Stream attributeKeyArgs() { + return Stream.of( + Arguments.of( + ExtendedAttributeKey.stringKey("key"), + "key", + ExtendedAttributeType.STRING, + AttributeKey.stringKey("key")), + Arguments.of( + ExtendedAttributeKey.booleanKey("key"), + "key", + ExtendedAttributeType.BOOLEAN, + AttributeKey.booleanKey("key")), + Arguments.of( + ExtendedAttributeKey.longKey("key"), + "key", + ExtendedAttributeType.LONG, + AttributeKey.longKey("key")), + Arguments.of( + ExtendedAttributeKey.doubleKey("key"), + "key", + ExtendedAttributeType.DOUBLE, + AttributeKey.doubleKey("key")), + Arguments.of( + ExtendedAttributeKey.stringArrayKey("key"), + "key", + ExtendedAttributeType.STRING_ARRAY, + AttributeKey.stringArrayKey("key")), + Arguments.of( + ExtendedAttributeKey.booleanArrayKey("key"), + "key", + ExtendedAttributeType.BOOLEAN_ARRAY, + AttributeKey.booleanArrayKey("key")), + Arguments.of( + ExtendedAttributeKey.longArrayKey("key"), + "key", + ExtendedAttributeType.LONG_ARRAY, + AttributeKey.longArrayKey("key")), + Arguments.of( + ExtendedAttributeKey.doubleArrayKey("key"), + "key", + ExtendedAttributeType.DOUBLE_ARRAY, + AttributeKey.doubleArrayKey("key")), + Arguments.of( + ExtendedAttributeKey.extendedAttributesKey("key"), + "key", + ExtendedAttributeType.EXTENDED_ATTRIBUTES, + null)); + } +} diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java new file mode 100644 index 00000000000..4481cead3d0 --- /dev/null +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/common/ExtendedAttributesTest.java @@ -0,0 +1,360 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.common; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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; + +class ExtendedAttributesTest { + + @ParameterizedTest + @MethodSource("attributesArgs") + void get_ExtendedAttributeKey( + ExtendedAttributes extendedAttributes, Map expectedMap) { + expectedMap.forEach( + (key, value) -> { + ExtendedAttributeKey extendedAttributeKey = getKey(key, value); + Object actualValue = extendedAttributes.get(extendedAttributeKey); + if (actualValue instanceof ExtendedAttributes) { + Map mapValue = toMap((ExtendedAttributes) actualValue); + actualValue = mapValue; + } + + assertThat(actualValue) + .describedAs(key + "(" + extendedAttributeKey.getType() + ")") + .isEqualTo(value); + }); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void get_AttributeKey(ExtendedAttributes extendedAttributes, Map expectedMap) { + expectedMap.forEach( + (key, value) -> { + ExtendedAttributeKey extendedAttributeKey = getKey(key, value); + AttributeKey attributeKey = extendedAttributeKey.asAttributeKey(); + + // Skip attribute keys which cannot be represented as AttributeKey + if (attributeKey == null) { + return; + } + + Object actualValue = extendedAttributes.get(attributeKey); + + assertThat(actualValue) + .describedAs(key + "(" + attributeKey.getType() + ")") + .isEqualTo(value); + }); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void forEach(ExtendedAttributes extendedAttributes, Map expectedMap) { + // toMap uses .forEach to convert + Map seenEntries = toMap(extendedAttributes); + + assertThat(seenEntries).isEqualTo(expectedMap); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void size(ExtendedAttributes extendedAttributes, Map expectedMap) { + assertThat(extendedAttributes.size()).isEqualTo(expectedMap.size()); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void isEmpty(ExtendedAttributes extendedAttributes, Map expectedMap) { + assertThat(extendedAttributes.isEmpty()).isEqualTo(expectedMap.isEmpty()); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void asMap(ExtendedAttributes extendedAttributes, Map expectedMap) { + assertEquals(extendedAttributes.asMap(), expectedMap); + } + + @SuppressWarnings("unchecked") + private static void assertEquals( + Map, Object> actual, Map expected) { + assertThat(actual.size()).isEqualTo(expected.size()); + actual.forEach( + (key, value) -> { + if (key.getType() == ExtendedAttributeType.EXTENDED_ATTRIBUTES) { + assertEquals( + ((ExtendedAttributes) value).asMap(), + (Map) expected.get(key.getKey())); + return; + } + assertThat(expected.get(key.getKey())).isEqualTo(value); + }); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void asAttributes(ExtendedAttributes extendedAttributes, Map expectedMap) { + Attributes attributes = extendedAttributes.asAttributes(); + + attributes.forEach( + (key, value) -> { + assertThat(value).isEqualTo(expectedMap.get(key.getKey())); + }); + + long expectedSize = + expectedMap.values().stream().filter(value -> !(value instanceof Map)).count(); + assertThat(attributes.size()).isEqualTo(expectedSize); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void toBuilder(ExtendedAttributes extendedAttributes, Map expectedMap) { + ExtendedAttributesBuilder builder = extendedAttributes.toBuilder(); + + builder.put("extraKey", "value"); + + ExtendedAttributes extendedAttributes1 = builder.build(); + assertThat(extendedAttributes1.size()).isEqualTo(expectedMap.size() + 1); + + ExtendedAttributes extendedAttributes2 = + extendedAttributes1.toBuilder().remove(ExtendedAttributeKey.stringKey("extraKey")).build(); + + assertThat(extendedAttributes2).isEqualTo(extendedAttributes); + assertThat(extendedAttributes2.size()).isEqualTo(expectedMap.size()); + } + + @ParameterizedTest + @MethodSource("attributesArgs") + void equalsAndHashcode(ExtendedAttributes extendedAttributes, Map expectedMap) { + ExtendedAttributes withExtraEntry = + extendedAttributes.toBuilder().put("extraKey", "value").build(); + assertThat(extendedAttributes).isNotEqualTo(withExtraEntry); + assertThat(extendedAttributes.hashCode()).isNotEqualTo(withExtraEntry.hashCode()); + + ExtendedAttributes copy1 = + extendedAttributes.toBuilder().remove(ExtendedAttributeKey.stringKey("extraKey")).build(); + assertThat(extendedAttributes).isEqualTo(copy1); + assertThat(extendedAttributes.hashCode()).isEqualTo(copy1.hashCode()); + + ExtendedAttributes copy2 = fromMap(expectedMap); + assertThat(extendedAttributes).isEqualTo(copy2); + assertThat(extendedAttributes.hashCode()).isEqualTo(copy2.hashCode()); + } + + @SuppressWarnings("unchecked") + private static ExtendedAttributes fromMap(Map map) { + ExtendedAttributesBuilder builder = ExtendedAttributes.builder(); + map.forEach( + (key, value) -> { + ExtendedAttributeKey extendedAttributeKey = getKey(key, value); + if (extendedAttributeKey.getType() == ExtendedAttributeType.EXTENDED_ATTRIBUTES) { + builder.put( + (ExtendedAttributeKey) extendedAttributeKey, + fromMap((Map) value)); + return; + } + putInBuilder((ExtendedAttributeKey) extendedAttributeKey, value, builder); + }); + return builder.build(); + } + + private static void putInBuilder( + ExtendedAttributeKey key, Object value, ExtendedAttributesBuilder builder) { + builder.put(key, value); + } + + private static Stream attributesArgs() { + return Stream.of( + // Single entry attributes + Arguments.of(ExtendedAttributes.builder().build(), Collections.emptyMap()), + Arguments.of( + ExtendedAttributes.builder().put("key", "value").build(), + ImmutableMap.builder().put("key", "value").build()), + Arguments.of( + ExtendedAttributes.builder().put("key", true).build(), + ImmutableMap.builder().put("key", true).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", 1L).build(), + ImmutableMap.builder().put("key", 1L).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", 1.1).build(), + ImmutableMap.builder().put("key", 1.1).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", "value1", "value2").build(), + ImmutableMap.builder().put("key", Arrays.asList("value1", "value2")).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", true, false).build(), + ImmutableMap.builder().put("key", Arrays.asList(true, false)).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", 1L, 2L).build(), + ImmutableMap.builder().put("key", Arrays.asList(1L, 2L)).build()), + Arguments.of( + ExtendedAttributes.builder().put("key", 1.1, 2.2).build(), + ImmutableMap.builder().put("key", Arrays.asList(1.1, 2.2)).build()), + Arguments.of( + ExtendedAttributes.builder() + .put("key", ExtendedAttributes.builder().put("child", "value").build()) + .build(), + ImmutableMap.builder() + .put("key", ImmutableMap.builder().put("child", "value").build()) + .build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.stringKey("key"), "value") + .build(), + ImmutableMap.builder().put("key", "value").build()), + Arguments.of( + ExtendedAttributes.builder().put(ExtendedAttributeKey.booleanKey("key"), true).build(), + ImmutableMap.builder().put("key", true).build()), + Arguments.of( + ExtendedAttributes.builder().put(ExtendedAttributeKey.longKey("key"), 1L).build(), + ImmutableMap.builder().put("key", 1L).build()), + Arguments.of( + ExtendedAttributes.builder().put(ExtendedAttributeKey.doubleKey("key"), 1.1).build(), + ImmutableMap.builder().put("key", 1.1).build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.stringArrayKey("key"), Arrays.asList("value1", "value2")) + .build(), + ImmutableMap.builder().put("key", Arrays.asList("value1", "value2")).build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.booleanArrayKey("key"), Arrays.asList(true, false)) + .build(), + ImmutableMap.builder().put("key", Arrays.asList(true, false)).build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.longArrayKey("key"), Arrays.asList(1L, 2L)) + .build(), + ImmutableMap.builder().put("key", Arrays.asList(1L, 2L)).build()), + Arguments.of( + ExtendedAttributes.builder() + .put(ExtendedAttributeKey.doubleArrayKey("key"), Arrays.asList(1.1, 2.2)) + .build(), + ImmutableMap.builder().put("key", Arrays.asList(1.1, 2.2)).build()), + Arguments.of( + ExtendedAttributes.builder() + .put( + ExtendedAttributeKey.extendedAttributesKey("key"), + ExtendedAttributes.builder().put("child", "value").build()) + .build(), + ImmutableMap.builder() + .put("key", ImmutableMap.builder().put("child", "value").build()) + .build()), + // Multiple entries + Arguments.of( + ExtendedAttributes.builder() + .put("key1", "value1") + .put("key2", "value2") + .put("key3", true) + .put("key4", 1L) + .put("key5", 1.1) + .put("key6", "value1", "value2") + .put("key7", true, false) + .put("key8", 1L, 2L) + .put("key9", 1.1, 2.2) + .put("key10", ExtendedAttributes.builder().put("child", "value").build()) + .build(), + ImmutableMap.builder() + .put("key1", "value1") + .put("key2", "value2") + .put("key3", true) + .put("key4", 1L) + .put("key5", 1.1) + .put("key6", Arrays.asList("value1", "value2")) + .put("key7", Arrays.asList(true, false)) + .put("key8", Arrays.asList(1L, 2L)) + .put("key9", Arrays.asList(1.1, 2.2)) + .put("key10", ImmutableMap.builder().put("child", "value").build()) + .build())); + } + + private static Map toMap(ExtendedAttributes extendedAttributes) { + Map map = new HashMap<>(); + extendedAttributes.forEach( + (key, value) -> { + if (key.getType() == ExtendedAttributeType.EXTENDED_ATTRIBUTES) { + map.put(key.getKey(), toMap((ExtendedAttributes) value)); + return; + } + map.put(key.getKey(), value); + }); + return map; + } + + private static ExtendedAttributeKey getKey(String key, Object value) { + switch (getType(value)) { + case STRING: + return ExtendedAttributeKey.stringKey(key); + case BOOLEAN: + return ExtendedAttributeKey.booleanKey(key); + case LONG: + return ExtendedAttributeKey.longKey(key); + case DOUBLE: + return ExtendedAttributeKey.doubleKey(key); + case STRING_ARRAY: + return ExtendedAttributeKey.stringArrayKey(key); + case BOOLEAN_ARRAY: + return ExtendedAttributeKey.booleanArrayKey(key); + case LONG_ARRAY: + return ExtendedAttributeKey.longArrayKey(key); + case DOUBLE_ARRAY: + return ExtendedAttributeKey.doubleArrayKey(key); + case EXTENDED_ATTRIBUTES: + return ExtendedAttributeKey.extendedAttributesKey(key); + } + throw new IllegalArgumentException(); + } + + @SuppressWarnings("unchecked") + private static ExtendedAttributeType getType(Object value) { + if (value instanceof String) { + return ExtendedAttributeType.STRING; + } + if (value instanceof Boolean) { + return ExtendedAttributeType.BOOLEAN; + } + if ((value instanceof Long) || (value instanceof Integer)) { + return ExtendedAttributeType.LONG; + } + if ((value instanceof Double) || (value instanceof Float)) { + return ExtendedAttributeType.DOUBLE; + } + if (value instanceof List) { + List list = (List) value; + if (list.isEmpty()) { + throw new IllegalArgumentException("Empty list"); + } + if (list.get(0) instanceof String) { + return ExtendedAttributeType.STRING_ARRAY; + } + if (list.get(0) instanceof Boolean) { + return ExtendedAttributeType.BOOLEAN_ARRAY; + } + if ((list.get(0) instanceof Long) || (list.get(0) instanceof Integer)) { + return ExtendedAttributeType.LONG_ARRAY; + } + if ((list.get(0) instanceof Double) || (list.get(0) instanceof Float)) { + return ExtendedAttributeType.DOUBLE_ARRAY; + } + } + if ((value instanceof Map)) { + return ExtendedAttributeType.EXTENDED_ATTRIBUTES; + } + throw new IllegalArgumentException("Unrecognized value type: " + value); + } +} diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java index 2d9c494ee3e..f7fdb511280 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/logs/ExtendedLogsBridgeApiUsageTest.java @@ -9,19 +9,30 @@ import static io.opentelemetry.sdk.logs.internal.LoggerConfig.disabled; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.util.Arrays; +import java.util.List; import java.util.Random; import org.junit.jupiter.api.Test; /** Demonstrating usage of extended Logs Bridge API. */ class ExtendedLogsBridgeApiUsageTest { + private static final java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(ExtendedLogsBridgeApiUsageTest.class.getName()); + @Test void loggerEnabled() { // Setup SdkLoggerProvider @@ -76,4 +87,148 @@ void loggerEnabled() { private static String flipCoin() { return random.nextBoolean() ? "heads" : "tails"; } + + // Primitive keys + AttributeKey strKey = AttributeKey.stringKey("acme.string"); + AttributeKey longKey = AttributeKey.longKey("acme.long"); + AttributeKey booleanKey = AttributeKey.booleanKey("acme.boolean"); + AttributeKey doubleKey = AttributeKey.doubleKey("acme.double"); + + // Primitive array keys + AttributeKey> strArrKey = AttributeKey.stringArrayKey("acme.string_array"); + AttributeKey> longArrKey = AttributeKey.longArrayKey("acme.long_array"); + AttributeKey> booleanArrKey = AttributeKey.booleanArrayKey("acme.boolean_array"); + AttributeKey> doubleArrKey = AttributeKey.doubleArrayKey("acme.double_array"); + + // Extended keys + ExtendedAttributeKey mapKey = + ExtendedAttributeKey.extendedAttributesKey("acme.map"); + + @Test + @SuppressLogger(ExtendedLogsBridgeApiUsageTest.class) + void extendedAttributesUsage() { + // Initialize from builder. Varargs style initialization (ExtendedAttributes.of(...) not + // supported. + ExtendedAttributes extendedAttributes = + ExtendedAttributes.builder() + .put(strKey, "value") + .put(longKey, 1L) + .put(booleanKey, true) + .put(doubleKey, 1.1) + .put(strArrKey, Arrays.asList("value1", "value2")) + .put(longArrKey, Arrays.asList(1L, 2L)) + .put(booleanArrKey, Arrays.asList(true, false)) + .put(doubleArrKey, Arrays.asList(1.1, 2.2)) + .put( + mapKey, + ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) + .build(); + + // Retrieval + assertThat(extendedAttributes.get(strKey)).isEqualTo("value"); + assertThat(extendedAttributes.get(longKey)).isEqualTo(1); + assertThat(extendedAttributes.get(booleanKey)).isEqualTo(true); + assertThat(extendedAttributes.get(doubleKey)).isEqualTo(1.1); + assertThat(extendedAttributes.get(strArrKey)).isEqualTo(Arrays.asList("value1", "value2")); + assertThat(extendedAttributes.get(longArrKey)).isEqualTo(Arrays.asList(1L, 2L)); + assertThat(extendedAttributes.get(booleanArrKey)).isEqualTo(Arrays.asList(true, false)); + assertThat(extendedAttributes.get(doubleArrKey)).isEqualTo(Arrays.asList(1.1, 2.2)); + assertThat(extendedAttributes.get(mapKey)) + .isEqualTo( + ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()); + + // Iteration + // Output: + // acme.boolean(BOOLEAN): true + // acme.boolean_array(BOOLEAN_ARRAY): [true, false] + // acme.double(DOUBLE): 1.1 + // acme.double_array(DOUBLE_ARRAY): [1.1, 2.2] + // acme.long(LONG): 1 + // acme.long_array(LONG_ARRAY): [1, 2] + // acme.map(EXTENDED_ATTRIBUTES): {childLong=1, childStr="value"} + // acme.string(STRING): value + // acme.string_array(STRING_ARRAY): [value1, value2] + extendedAttributes.forEach( + (extendedAttributeKey, object) -> + logger.info( + String.format( + "%s(%s): %s", + extendedAttributeKey.getKey(), extendedAttributeKey.getType(), object))); + } + + @Test + @SuppressWarnings("deprecation") // testing deprecated code + void logRecordBuilder_ExtendedAttributes() { + InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); + SdkLoggerProvider loggerProvider = + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)) + .build(); + + Logger logger = loggerProvider.get("logger"); + + // Can set either standard or extended attributes on + ((ExtendedLogRecordBuilder) logger.logRecordBuilder()) + .setBody("message") + .setAttribute(strKey, "value") + .setAttribute(longKey, 1L) + .setAttribute(booleanKey, true) + .setAttribute(doubleKey, 1.1) + .setAttribute(strArrKey, Arrays.asList("value1", "value2")) + .setAttribute(longArrKey, Arrays.asList(1L, 2L)) + .setAttribute(booleanArrKey, Arrays.asList(true, false)) + .setAttribute(doubleArrKey, Arrays.asList(1.1, 2.2)) + .setAttribute( + mapKey, + ExtendedAttributes.builder().put("childStr", "value").put("childLong", 1L).build()) + .setAllAttributes(Attributes.builder().put("key1", "value").build()) + .setAllAttributes(ExtendedAttributes.builder().put("key2", "value").build()) + .emit(); + + assertThat(exporter.getFinishedLogRecordItems()) + .satisfiesExactly( + logRecordData -> { + assertThat(logRecordData).isInstanceOf(ExtendedLogRecordData.class); + ExtendedLogRecordData extendedLogRecordData = (ExtendedLogRecordData) logRecordData; + + // Optionally access standard attributes, which filters out any extended attribute + // types + assertThat(extendedLogRecordData.getAttributes()) + .isEqualTo( + Attributes.builder() + .put(strKey, "value") + .put(longKey, 1L) + .put(booleanKey, true) + .put(doubleKey, 1.1) + .put(strArrKey, Arrays.asList("value1", "value2")) + .put(longArrKey, Arrays.asList(1L, 2L)) + .put(booleanArrKey, Arrays.asList(true, false)) + .put(doubleArrKey, Arrays.asList(1.1, 2.2)) + .put("key1", "value") + .put("key2", "value") + .build()); + + // But preferably access and serialize full extended attributes + assertThat(extendedLogRecordData.getExtendedAttributes()) + .isEqualTo( + ExtendedAttributes.builder() + .put(strKey, "value") + .put(longKey, 1L) + .put(booleanKey, true) + .put(doubleKey, 1.1) + .put(strArrKey, Arrays.asList("value1", "value2")) + .put(longArrKey, Arrays.asList(1L, 2L)) + .put(booleanArrKey, Arrays.asList(true, false)) + .put(doubleArrKey, Arrays.asList(1.1, 2.2)) + .put( + mapKey, + ExtendedAttributes.builder() + .put("childStr", "value") + .put("childLong", 1L) + .build()) + .put("key1", "value") + .put("key2", "value") + .build()); + }); + } } diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/CodedOutputStream.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/CodedOutputStream.java index 68311be1845..c9f12923181 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/CodedOutputStream.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/CodedOutputStream.java @@ -214,7 +214,7 @@ static int computeInt32SizeNoTag(final int value) { } /** Compute the number of bytes that would be needed to encode a {@code uint32} field. */ - static int computeUInt32SizeNoTag(final int value) { + public static int computeUInt32SizeNoTag(final int value) { if ((value & (~0 << 7)) == 0) { return 1; } 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 6170e1925d2..62b391c8905 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 @@ -220,23 +220,23 @@ public void serializeRepeatedMessageWithContext( } @Override - protected void writeStartRepeated(ProtoFieldInfo field) throws IOException { + public void writeStartRepeated(ProtoFieldInfo field) throws IOException { generator.writeArrayFieldStart(field.getJsonName()); } @Override - protected void writeEndRepeated() throws IOException { + public void writeEndRepeated() throws IOException { generator.writeEndArray(); } @Override - protected void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) + public void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) throws IOException { generator.writeStartObject(); } @Override - protected void writeEndRepeatedElement() throws IOException { + public void writeEndRepeatedElement() throws IOException { generator.writeEndObject(); } 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 694cec8b2b9..4094f8b01ac 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 @@ -249,23 +249,23 @@ public void serializeRepeatedMessageWithContext( } @Override - protected void writeStartRepeated(ProtoFieldInfo field) { + public void writeStartRepeated(ProtoFieldInfo field) { // Do nothing } @Override - protected void writeEndRepeated() { + public void writeEndRepeated() { // Do nothing } @Override - protected void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) + public void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) throws IOException { writeStartMessage(field, protoMessageSize); } @Override - protected void writeEndRepeatedElement() { + public void writeEndRepeatedElement() { writeEndMessage(); } 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 100506fb3dc..e8e549544ca 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 @@ -674,17 +674,17 @@ public void accept(K key, V value) { } /** Writes start of repeated messages. */ - protected abstract void writeStartRepeated(ProtoFieldInfo field) throws IOException; + public abstract void writeStartRepeated(ProtoFieldInfo field) throws IOException; /** Writes end of repeated messages. */ - protected abstract void writeEndRepeated() throws IOException; + public abstract void writeEndRepeated() throws IOException; /** Writes start of a repeated message element. */ - protected abstract void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) + public abstract void writeStartRepeatedElement(ProtoFieldInfo field, int protoMessageSize) throws IOException; /** Writes end of a repeated message element. */ - protected abstract void writeEndRepeatedElement() throws IOException; + public abstract void writeEndRepeatedElement() throws IOException; /** Writes the value for a message field that has been pre-serialized. */ public abstract void writeSerializedMessage(byte[] protoSerialized, String jsonSerialized) diff --git a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java index 8ef1c4ace44..f6f13467887 100644 --- a/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java +++ b/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/TestDataExporter.java @@ -27,7 +27,6 @@ import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.logs.TestLogRecordData; import io.opentelemetry.sdk.testing.logs.internal.TestExtendedLogRecordData; import io.opentelemetry.sdk.testing.trace.TestSpanData; import io.opentelemetry.sdk.trace.data.EventData; @@ -55,7 +54,6 @@ abstract class TestDataExporter { .setVersion("1") .setAttributes(Attributes.builder().put("key", "value").build()) .build()) - .setEventName("event name") .setBody("body1") .setSeverity(Severity.INFO) .setSeverityText("INFO") @@ -72,7 +70,7 @@ abstract class TestDataExporter { .build(); private static final LogRecordData LOG2 = - TestLogRecordData.builder() + TestExtendedLogRecordData.builder() .setResource(RESOURCE) .setInstrumentationScopeInfo( InstrumentationScopeInfo.builder("instrumentation2").setVersion("2").build()) diff --git a/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json index 65767985bc2..6557b719863 100644 --- a/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json +++ b/exporters/logging-otlp/src/test/resources/expected-logs-wrapper.json @@ -27,7 +27,6 @@ }, "logRecords": [ { - "eventName": "event name", "timeUnixNano": "100", "observedTimeUnixNano": "200", "severityNumber": 9, diff --git a/exporters/logging-otlp/src/test/resources/expected-logs.json b/exporters/logging-otlp/src/test/resources/expected-logs.json index f9bb36dd273..b1d46cc8f5e 100644 --- a/exporters/logging-otlp/src/test/resources/expected-logs.json +++ b/exporters/logging-otlp/src/test/resources/expected-logs.json @@ -25,7 +25,6 @@ }, "logRecords": [ { - "eventName": "event name", "timeUnixNano": "100", "observedTimeUnixNano": "200", "severityNumber": 9, diff --git a/exporters/otlp/common/build.gradle.kts b/exporters/otlp/common/build.gradle.kts index 94d50e46fd0..5ad623561b2 100644 --- a/exporters/otlp/common/build.gradle.kts +++ b/exporters/otlp/common/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { compileOnly(project(":sdk:metrics")) compileOnly(project(":sdk:trace")) compileOnly(project(":sdk:logs")) + compileOnly(project(":api:incubator")) testImplementation(project(":sdk:metrics")) testImplementation(project(":sdk:trace")) @@ -40,6 +41,22 @@ dependencies { jmhImplementation("io.grpc:grpc-netty") } +testing { + suites { + register("testIncubating") { + dependencies { + implementation(project(":api:incubator")) + implementation(project(":sdk:testing")) + + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.google.protobuf:protobuf-java-util") + implementation("com.google.guava:guava") + implementation("io.opentelemetry.proto:opentelemetry-proto") + } + } + } +} + wire { root( "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest", diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java new file mode 100644 index 00000000000..de31139abcb --- /dev/null +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ExtendedAttributeKeyValueStatelessMarshaler.java @@ -0,0 +1,248 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributeType; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; +import io.opentelemetry.exporter.internal.marshal.CodedOutputStream; +import io.opentelemetry.exporter.internal.marshal.MarshalerContext; +import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; +import io.opentelemetry.exporter.internal.marshal.ProtoFieldInfo; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.exporter.internal.marshal.StatelessMarshaler; +import io.opentelemetry.exporter.internal.marshal.StatelessMarshaler2; +import io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil; +import io.opentelemetry.proto.common.v1.internal.AnyValue; +import io.opentelemetry.proto.common.v1.internal.KeyValue; +import io.opentelemetry.proto.common.v1.internal.KeyValueList; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Objects; + +/** + * A Marshaler of {@link ExtendedAttributes} key value pairs. See {@link KeyValueMarshaler}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ExtendedAttributeKeyValueStatelessMarshaler + implements StatelessMarshaler2, Object> { + private static final ExtendedAttributeKeyValueStatelessMarshaler INSTANCE = + new ExtendedAttributeKeyValueStatelessMarshaler(); + private static final byte[] EMPTY_BYTES = new byte[0]; + + private ExtendedAttributeKeyValueStatelessMarshaler() {} + + /** + * Serializes the {@code attributes}. This method reads elements from context, use together with + * {@link ExtendedAttributeKeyValueStatelessMarshaler#sizeExtendedAttributes(ProtoFieldInfo, + * ExtendedAttributes, MarshalerContext)}. + */ + public static void serializeExtendedAttributes( + Serializer output, + ProtoFieldInfo field, + ExtendedAttributes attributes, + MarshalerContext context) + throws IOException { + output.writeStartRepeated(field); + + if (!attributes.isEmpty()) { + try { + attributes.forEach( + (extendedAttributeKey, value) -> { + try { + output.writeStartRepeatedElement(field, context.getSize()); + INSTANCE.writeTo(output, extendedAttributeKey, value, context); + output.writeEndRepeatedElement(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + output.writeEndRepeated(); + } + + /** + * Sizes the {@code attributes}. This method adds elements to context, use together with {@link + * ExtendedAttributeKeyValueStatelessMarshaler#serializeExtendedAttributes(Serializer, + * ProtoFieldInfo, ExtendedAttributes, MarshalerContext)}. + */ + public static int sizeExtendedAttributes( + ProtoFieldInfo field, ExtendedAttributes attributes, MarshalerContext context) { + if (attributes.isEmpty()) { + return 0; + } + + int[] size = new int[] {0}; + + attributes.forEach( + (extendedAttributeKey, value) -> { + int sizeIndex = context.addSize(); + int fieldSize = INSTANCE.getBinarySerializedSize(extendedAttributeKey, value, context); + context.setSize(sizeIndex, fieldSize); + size[0] += + field.getTagSize() + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + }); + + return size[0]; + } + + @Override + public void writeTo( + Serializer output, + ExtendedAttributeKey attributeKey, + Object value, + MarshalerContext context) + throws IOException { + if (attributeKey.getKey().isEmpty()) { + output.serializeString(KeyValue.KEY, EMPTY_BYTES); + } else if (attributeKey instanceof InternalExtendedAttributeKeyImpl) { + byte[] keyUtf8 = ((InternalExtendedAttributeKeyImpl) attributeKey).getKeyUtf8(); + output.serializeString(KeyValue.KEY, keyUtf8); + } else { + output.serializeStringWithContext(KeyValue.KEY, attributeKey.getKey(), context); + } + output.serializeMessageWithContext( + KeyValue.VALUE, attributeKey, value, ValueStatelessMarshaler.INSTANCE, context); + } + + @Override + public int getBinarySerializedSize( + ExtendedAttributeKey attributeKey, Object value, MarshalerContext context) { + int size = 0; + if (!attributeKey.getKey().isEmpty()) { + if (attributeKey instanceof InternalExtendedAttributeKeyImpl) { + byte[] keyUtf8 = ((InternalExtendedAttributeKeyImpl) attributeKey).getKeyUtf8(); + size += MarshalerUtil.sizeBytes(KeyValue.KEY, keyUtf8); + } else { + return StatelessMarshalerUtil.sizeStringWithContext( + KeyValue.KEY, attributeKey.getKey(), context); + } + } + size += + StatelessMarshalerUtil.sizeMessageWithContext( + KeyValue.VALUE, attributeKey, value, ValueStatelessMarshaler.INSTANCE, context); + + return size; + } + + private static class ValueStatelessMarshaler + implements StatelessMarshaler2, Object> { + static final ValueStatelessMarshaler INSTANCE = new ValueStatelessMarshaler(); + + @SuppressWarnings("unchecked") + @Override + public int getBinarySerializedSize( + ExtendedAttributeKey attributeKey, Object value, MarshalerContext context) { + ExtendedAttributeType attributeType = attributeKey.getType(); + switch (attributeType) { + case STRING: + return StringAnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (String) value, context); + case LONG: + return IntAnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (Long) value, context); + case BOOLEAN: + return BoolAnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (Boolean) value, context); + case DOUBLE: + return DoubleAnyValueStatelessMarshaler.INSTANCE.getBinarySerializedSize( + (Double) value, context); + case STRING_ARRAY: + case LONG_ARRAY: + case BOOLEAN_ARRAY: + case DOUBLE_ARRAY: + return StatelessMarshalerUtil.sizeMessageWithContext( + AnyValue.ARRAY_VALUE, + Objects.requireNonNull(attributeKey.asAttributeKey()).getType(), + (List) value, + AttributeArrayAnyValueStatelessMarshaler.INSTANCE, + context); + case EXTENDED_ATTRIBUTES: + return StatelessMarshalerUtil.sizeMessageWithContext( + AnyValue.KVLIST_VALUE, + (ExtendedAttributes) value, + ExtendedAttributesKeyValueListStatelessMarshaler.INSTANCE, + context); + } + // Error prone ensures the switch statement is complete, otherwise only can happen with + // unaligned versions which are not supported. + throw new IllegalArgumentException("Unsupported attribute type."); + } + + @SuppressWarnings("unchecked") + @Override + public void writeTo( + Serializer output, + ExtendedAttributeKey attributeKey, + Object value, + MarshalerContext context) + throws IOException { + ExtendedAttributeType attributeType = attributeKey.getType(); + switch (attributeType) { + case STRING: + StringAnyValueStatelessMarshaler.INSTANCE.writeTo(output, (String) value, context); + return; + case LONG: + IntAnyValueStatelessMarshaler.INSTANCE.writeTo(output, (Long) value, context); + return; + case BOOLEAN: + BoolAnyValueStatelessMarshaler.INSTANCE.writeTo(output, (Boolean) value, context); + return; + case DOUBLE: + DoubleAnyValueStatelessMarshaler.INSTANCE.writeTo(output, (Double) value, context); + return; + case STRING_ARRAY: + case LONG_ARRAY: + case BOOLEAN_ARRAY: + case DOUBLE_ARRAY: + output.serializeMessageWithContext( + AnyValue.ARRAY_VALUE, + Objects.requireNonNull(attributeKey.asAttributeKey()).getType(), + (List) value, + AttributeArrayAnyValueStatelessMarshaler.INSTANCE, + context); + return; + case EXTENDED_ATTRIBUTES: + output.serializeMessageWithContext( + AnyValue.KVLIST_VALUE, + (ExtendedAttributes) value, + ExtendedAttributesKeyValueListStatelessMarshaler.INSTANCE, + context); + return; + } + // Error prone ensures the switch statement is complete, otherwise only can happen with + // unaligned versions which are not supported. + throw new IllegalArgumentException("Unsupported attribute type."); + } + } + + private static class ExtendedAttributesKeyValueListStatelessMarshaler + implements StatelessMarshaler { + private static final ExtendedAttributesKeyValueListStatelessMarshaler INSTANCE = + new ExtendedAttributesKeyValueListStatelessMarshaler(); + + private ExtendedAttributesKeyValueListStatelessMarshaler() {} + + @Override + public void writeTo(Serializer output, ExtendedAttributes value, MarshalerContext context) + throws IOException { + serializeExtendedAttributes(output, KeyValueList.VALUES, value, context); + } + + @Override + public int getBinarySerializedSize(ExtendedAttributes value, MarshalerContext context) { + return sizeExtendedAttributes(KeyValueList.VALUES, value, context); + } + } +} diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java new file mode 100644 index 00000000000..fb1e350188a --- /dev/null +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/IncubatingUtil.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.incubator.internal.InternalExtendedAttributeKeyImpl; +import io.opentelemetry.exporter.internal.marshal.MarshalerContext; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.proto.logs.v1.internal.LogRecord; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * Utilities for interacting with {@code io.opentelemetry:opentelemetry-api-incubator}, which is not + * guaranteed to be present on the classpath. For all methods, callers MUST first separately + * reflectively confirm that the incubator is available on the classpath. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class IncubatingUtil { + + private static final byte[] EMPTY_BYTES = new byte[0]; + private static final KeyValueMarshaler[] EMPTY_REPEATED = new KeyValueMarshaler[0]; + + private IncubatingUtil() {} + + @SuppressWarnings("AvoidObjectArrays") + public static KeyValueMarshaler[] createdExtendedAttributesMarhsalers( + LogRecordData logRecordData) { + return createForExtendedAttributes(getExtendedAttributes(logRecordData)); + } + + public static int extendedAttributesSize(LogRecordData logRecordData) { + return getExtendedAttributes(logRecordData).size(); + } + + // TODO(jack-berg): move to KeyValueMarshaler when ExtendedAttributes is stable + private static KeyValueMarshaler[] createForExtendedAttributes(ExtendedAttributes attributes) { + if (attributes.isEmpty()) { + return EMPTY_REPEATED; + } + + KeyValueMarshaler[] marshalers = new KeyValueMarshaler[attributes.size()]; + attributes.forEach( + new BiConsumer, Object>() { + int index = 0; + + @Override + public void accept(ExtendedAttributeKey attributeKey, Object o) { + marshalers[index++] = create(attributeKey, o); + } + }); + return marshalers; + } + + // TODO(jack-berg): move to KeyValueMarshaler when ExtendedAttributes is stable + @SuppressWarnings("unchecked") + private static KeyValueMarshaler create(ExtendedAttributeKey attributeKey, Object value) { + byte[] keyUtf8; + if (attributeKey.getKey().isEmpty()) { + keyUtf8 = EMPTY_BYTES; + } else if (attributeKey instanceof InternalExtendedAttributeKeyImpl) { + keyUtf8 = ((InternalExtendedAttributeKeyImpl) attributeKey).getKeyUtf8(); + } else { + keyUtf8 = attributeKey.getKey().getBytes(StandardCharsets.UTF_8); + } + switch (attributeKey.getType()) { + case STRING: + return new KeyValueMarshaler(keyUtf8, StringAnyValueMarshaler.create((String) value)); + case LONG: + return new KeyValueMarshaler(keyUtf8, IntAnyValueMarshaler.create((long) value)); + case BOOLEAN: + return new KeyValueMarshaler(keyUtf8, BoolAnyValueMarshaler.create((boolean) value)); + case DOUBLE: + return new KeyValueMarshaler(keyUtf8, DoubleAnyValueMarshaler.create((double) value)); + case STRING_ARRAY: + return new KeyValueMarshaler( + keyUtf8, ArrayAnyValueMarshaler.createString((List) value)); + case LONG_ARRAY: + return new KeyValueMarshaler(keyUtf8, ArrayAnyValueMarshaler.createInt((List) value)); + case BOOLEAN_ARRAY: + return new KeyValueMarshaler( + keyUtf8, ArrayAnyValueMarshaler.createBool((List) value)); + case DOUBLE_ARRAY: + return new KeyValueMarshaler( + keyUtf8, ArrayAnyValueMarshaler.createDouble((List) value)); + case EXTENDED_ATTRIBUTES: + return new KeyValueMarshaler( + keyUtf8, + new KeyValueListAnyValueMarshaler( + new KeyValueListAnyValueMarshaler.KeyValueListMarshaler( + createForExtendedAttributes((ExtendedAttributes) value)))); + } + // Error prone ensures the switch statement is complete, otherwise only can happen with + // unaligned versions which are not supported. + throw new IllegalArgumentException("Unsupported attribute type."); + } + + public static int sizeExtendedAttributes(LogRecordData log, MarshalerContext context) { + return ExtendedAttributeKeyValueStatelessMarshaler.sizeExtendedAttributes( + LogRecord.ATTRIBUTES, getExtendedAttributes(log), context); + } + + public static void serializeExtendedAttributes( + Serializer output, LogRecordData log, MarshalerContext context) throws IOException { + ExtendedAttributeKeyValueStatelessMarshaler.serializeExtendedAttributes( + output, LogRecord.ATTRIBUTES, getExtendedAttributes(log), context); + } + + private static ExtendedAttributes getExtendedAttributes(LogRecordData logRecordData) { + if (!(logRecordData instanceof ExtendedLogRecordData)) { + throw new IllegalArgumentException("logRecordData must be ExtendedLogRecordData"); + } + return ((ExtendedLogRecordData) logRecordData).getExtendedAttributes(); + } +} diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueListAnyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueListAnyValueMarshaler.java index 1e5b345acae..1fb20cc269c 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueListAnyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueListAnyValueMarshaler.java @@ -19,7 +19,7 @@ final class KeyValueListAnyValueMarshaler extends MarshalerWithSize { private final Marshaler value; - private KeyValueListAnyValueMarshaler(KeyValueListMarshaler value) { + KeyValueListAnyValueMarshaler(KeyValueListMarshaler value) { super(calculateSize(value)); this.value = value; } @@ -42,11 +42,11 @@ private static int calculateSize(Marshaler value) { return MarshalerUtil.sizeMessage(AnyValue.KVLIST_VALUE, value); } - private static class KeyValueListMarshaler extends MarshalerWithSize { + static class KeyValueListMarshaler extends MarshalerWithSize { private final Marshaler[] values; - private KeyValueListMarshaler(KeyValueMarshaler[] values) { + KeyValueListMarshaler(KeyValueMarshaler[] values) { super(calculateSize(values)); this.values = values; } diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java index 08b9699291d..ec7dd47f10b 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/KeyValueMarshaler.java @@ -33,7 +33,7 @@ public final class KeyValueMarshaler extends MarshalerWithSize { private final byte[] keyUtf8; private final Marshaler value; - private KeyValueMarshaler(byte[] keyUtf8, Marshaler value) { + KeyValueMarshaler(byte[] keyUtf8, Marshaler value) { super(calculateSize(keyUtf8, value)); this.keyUtf8 = keyUtf8; this.value = value; diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java index 0ad265e9655..343d158cf8c 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java @@ -15,6 +15,7 @@ import io.opentelemetry.exporter.internal.marshal.ProtoEnumInfo; import io.opentelemetry.exporter.internal.marshal.Serializer; import io.opentelemetry.exporter.internal.otlp.AnyValueMarshaler; +import io.opentelemetry.exporter.internal.otlp.IncubatingUtil; import io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler; import io.opentelemetry.proto.logs.v1.internal.LogRecord; import io.opentelemetry.proto.logs.v1.internal.SeverityNumber; @@ -24,6 +25,19 @@ import javax.annotation.Nullable; final class LogMarshaler extends MarshalerWithSize { + private static final boolean INCUBATOR_AVAILABLE; + + static { + boolean incubatorAvailable = false; + try { + Class.forName("io.opentelemetry.api.incubator.common.ExtendedAttributes"); + incubatorAvailable = true; + } catch (ClassNotFoundException e) { + // Not available + } + INCUBATOR_AVAILABLE = incubatorAvailable; + } + private static final String INVALID_TRACE_ID = TraceId.getInvalid(); private static final String INVALID_SPAN_ID = SpanId.getInvalid(); private static final byte[] EMPTY_BYTES = new byte[0]; @@ -42,7 +56,14 @@ final class LogMarshaler extends MarshalerWithSize { static LogMarshaler create(LogRecordData logRecordData) { KeyValueMarshaler[] attributeMarshalers = - KeyValueMarshaler.createForAttributes(logRecordData.getAttributes()); + INCUBATOR_AVAILABLE + ? IncubatingUtil.createdExtendedAttributesMarhsalers(logRecordData) + : KeyValueMarshaler.createForAttributes(logRecordData.getAttributes()); + + int attributeSize = + INCUBATOR_AVAILABLE + ? IncubatingUtil.extendedAttributesSize(logRecordData) + : logRecordData.getAttributes().size(); MarshalerWithSize bodyMarshaler = logRecordData.getBodyValue() == null @@ -57,7 +78,7 @@ static LogMarshaler create(LogRecordData logRecordData) { MarshalerUtil.toBytes(logRecordData.getSeverityText()), bodyMarshaler, attributeMarshalers, - logRecordData.getTotalAttributeCount() - logRecordData.getAttributes().size(), + logRecordData.getTotalAttributeCount() - attributeSize, spanContext.getTraceFlags(), spanContext.getTraceId().equals(INVALID_TRACE_ID) ? null : spanContext.getTraceId(), spanContext.getSpanId().equals(INVALID_SPAN_ID) ? null : spanContext.getSpanId(), diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogStatelessMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogStatelessMarshaler.java index 1477af88ce6..331f7440a04 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogStatelessMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogStatelessMarshaler.java @@ -17,6 +17,7 @@ import io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil; import io.opentelemetry.exporter.internal.otlp.AnyValueStatelessMarshaler; import io.opentelemetry.exporter.internal.otlp.AttributeKeyValueStatelessMarshaler; +import io.opentelemetry.exporter.internal.otlp.IncubatingUtil; import io.opentelemetry.proto.logs.v1.internal.LogRecord; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; @@ -24,8 +25,22 @@ /** See {@link LogMarshaler}. */ final class LogStatelessMarshaler implements StatelessMarshaler { + private static final String INVALID_TRACE_ID = TraceId.getInvalid(); private static final String INVALID_SPAN_ID = SpanId.getInvalid(); + private static final boolean INCUBATOR_AVAILABLE; + + static { + boolean incubatorAvailable = false; + try { + Class.forName("io.opentelemetry.api.incubator.common.ExtendedAttributes"); + incubatorAvailable = true; + } catch (ClassNotFoundException e) { + // Not available + } + INCUBATOR_AVAILABLE = incubatorAvailable; + } + static final LogStatelessMarshaler INSTANCE = new LogStatelessMarshaler(); @Override @@ -40,12 +55,20 @@ public void writeTo(Serializer output, LogRecordData log, MarshalerContext conte output.serializeMessageWithContext( LogRecord.BODY, log.getBodyValue(), AnyValueStatelessMarshaler.INSTANCE, context); } - output.serializeRepeatedMessageWithContext( - LogRecord.ATTRIBUTES, - log.getAttributes(), - AttributeKeyValueStatelessMarshaler.INSTANCE, - context); - int droppedAttributesCount = log.getTotalAttributeCount() - log.getAttributes().size(); + + int droppedAttributesCount; + if (INCUBATOR_AVAILABLE) { + IncubatingUtil.serializeExtendedAttributes(output, log, context); + droppedAttributesCount = + log.getTotalAttributeCount() - IncubatingUtil.extendedAttributesSize(log); + } else { + output.serializeRepeatedMessageWithContext( + LogRecord.ATTRIBUTES, + log.getAttributes(), + AttributeKeyValueStatelessMarshaler.INSTANCE, + context); + droppedAttributesCount = log.getTotalAttributeCount() - log.getAttributes().size(); + } output.serializeUInt32(LogRecord.DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount); SpanContext spanContext = log.getSpanContext(); @@ -80,14 +103,23 @@ public int getBinarySerializedSize(LogRecordData log, MarshalerContext context) StatelessMarshalerUtil.sizeMessageWithContext( LogRecord.BODY, log.getBodyValue(), AnyValueStatelessMarshaler.INSTANCE, context); } - size += - StatelessMarshalerUtil.sizeRepeatedMessageWithContext( - LogRecord.ATTRIBUTES, - log.getAttributes(), - AttributeKeyValueStatelessMarshaler.INSTANCE, - context); - int droppedAttributesCount = log.getTotalAttributeCount() - log.getAttributes().size(); - size += MarshalerUtil.sizeUInt32(LogRecord.DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount); + if (INCUBATOR_AVAILABLE) { + size += IncubatingUtil.sizeExtendedAttributes(log, context); + + int droppedAttributesCount = + log.getTotalAttributeCount() - IncubatingUtil.extendedAttributesSize(log); + size += MarshalerUtil.sizeUInt32(LogRecord.DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount); + } else { + size += + StatelessMarshalerUtil.sizeRepeatedMessageWithContext( + LogRecord.ATTRIBUTES, + log.getAttributes(), + AttributeKeyValueStatelessMarshaler.INSTANCE, + context); + + int droppedAttributesCount = log.getTotalAttributeCount() - log.getAttributes().size(); + size += MarshalerUtil.sizeUInt32(LogRecord.DROPPED_ATTRIBUTES_COUNT, droppedAttributesCount); + } SpanContext spanContext = log.getSpanContext(); size += MarshalerUtil.sizeFixed32(LogRecord.FLAGS, spanContext.getTraceFlags().asByte()); diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerTest.java index 0e54a10f0e2..9c01ddc7aed 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerTest.java @@ -33,7 +33,7 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.logs.internal.TestExtendedLogRecordData; +import io.opentelemetry.sdk.testing.logs.TestLogRecordData; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; @@ -52,7 +52,6 @@ class LogsRequestMarshalerTest { private static final String TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES); private static final byte[] SPAN_ID_BYTES = new byte[] {0, 0, 0, 0, 4, 3, 2, 1}; private static final String SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES); - private static final String EVENT_NAME = "hello"; private static final String BODY = "Hello world from this log..."; @Test @@ -60,7 +59,7 @@ void toProtoResourceLogs() { ResourceLogsMarshaler[] resourceLogsMarshalers = ResourceLogsMarshaler.create( Collections.singleton( - TestExtendedLogRecordData.builder() + TestLogRecordData.builder() .setResource( Resource.builder().put("one", 1).setSchemaUrl("http://url").build()) .setInstrumentationScopeInfo( @@ -69,7 +68,6 @@ void toProtoResourceLogs() { .setSchemaUrl("http://url") .setAttributes(Attributes.builder().put("key", "value").build()) .build()) - .setEventName(EVENT_NAME) .setBody(BODY) .setSeverity(Severity.INFO) .setSeverityText("INFO") @@ -110,12 +108,11 @@ void toProtoLogRecord(MarshalerSource marshalerSource) { parse( LogRecord.getDefaultInstance(), marshalerSource.create( - TestExtendedLogRecordData.builder() + TestLogRecordData.builder() .setResource( Resource.create(Attributes.builder().put("testKey", "testValue").build())) .setInstrumentationScopeInfo( InstrumentationScopeInfo.builder("instrumentation").setVersion("1").build()) - .setEventName(EVENT_NAME) .setBody(BODY) .setSeverity(Severity.INFO) .setSeverityText("INFO") @@ -131,7 +128,6 @@ void toProtoLogRecord(MarshalerSource marshalerSource) { assertThat(logRecord.getTraceId().toByteArray()).isEqualTo(TRACE_ID_BYTES); assertThat(logRecord.getSpanId().toByteArray()).isEqualTo(SPAN_ID_BYTES); assertThat(logRecord.getSeverityText()).isEqualTo("INFO"); - assertThat(logRecord.getEventName()).isEqualTo(EVENT_NAME); assertThat(logRecord.getBody()).isEqualTo(AnyValue.newBuilder().setStringValue(BODY).build()); assertThat(logRecord.getAttributesList()) .containsExactly( @@ -151,7 +147,7 @@ void toProtoLogRecord_MinimalFields(MarshalerSource marshalerSource) { parse( LogRecord.getDefaultInstance(), marshalerSource.create( - TestExtendedLogRecordData.builder() + TestLogRecordData.builder() .setResource( Resource.create(Attributes.builder().put("testKey", "testValue").build())) .setInstrumentationScopeInfo( diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java index 2d8d61e207d..4890e02dd66 100644 --- a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/logs/LowAllocationLogRequestMarshalerTest.java @@ -16,7 +16,7 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.logs.internal.TestExtendedLogRecordData; +import io.opentelemetry.sdk.testing.logs.TestLogRecordData; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -39,7 +39,6 @@ class LowAllocationLogRequestMarshalerTest { AttributeKey.doubleArrayKey("key_double_array"); private static final AttributeKey> KEY_BOOLEAN_ARRAY = AttributeKey.booleanArrayKey("key_boolean_array"); - private static final String EVENT_NAME = "hello"; private static final String BODY = "Hello world from this log..."; private static final Resource RESOURCE = @@ -73,10 +72,9 @@ private static List createLogRecordDataList() { } private static LogRecordData createLogRecordData() { - return TestExtendedLogRecordData.builder() + return TestLogRecordData.builder() .setResource(RESOURCE) .setInstrumentationScopeInfo(INSTRUMENTATION_SCOPE_INFO) - .setEventName(EVENT_NAME) .setBody(BODY) .setSeverity(Severity.INFO) .setSeverityText("INFO") diff --git a/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java new file mode 100644 index 00000000000..8b4533e24ae --- /dev/null +++ b/exporters/otlp/common/src/testIncubating/java/io/opentelemetry/exporter/internal/otlp/logs/LogsRequestMarshalerIncubatingTest.java @@ -0,0 +1,292 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp.logs; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import io.opentelemetry.exporter.internal.marshal.MarshalerContext; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.exporter.internal.marshal.StatelessMarshaler; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.common.v1.KeyValueList; +import io.opentelemetry.proto.logs.v1.LogRecord; +import io.opentelemetry.proto.logs.v1.ResourceLogs; +import io.opentelemetry.proto.logs.v1.ScopeLogs; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.logs.internal.TestExtendedLogRecordData; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class LogsRequestMarshalerIncubatingTest { + private static final byte[] TRACE_ID_BYTES = + new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}; + private static final String TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES); + private static final byte[] SPAN_ID_BYTES = new byte[] {0, 0, 0, 0, 4, 3, 2, 1}; + private static final String SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES); + private static final String EVENT_NAME = "hello"; + private static final String BODY = "Hello world from this log..."; + + @ParameterizedTest + @EnumSource(MarshalerSource.class) + void toProtoLogRecord(MarshalerSource marshalerSource) { + LogRecord logRecord = + parse( + LogRecord.getDefaultInstance(), + marshalerSource.create( + TestExtendedLogRecordData.builder() + .setResource( + Resource.create(Attributes.builder().put("testKey", "testValue").build())) + .setInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("instrumentation").setVersion("1").build()) + .setBody(BODY) + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setTotalAttributeCount(10) + .setTimestamp(12345, TimeUnit.NANOSECONDS) + .setObservedTimestamp(6789, TimeUnit.NANOSECONDS) + // Extended fields + .setEventName(EVENT_NAME) + .setExtendedAttributes( + ExtendedAttributes.builder() + .put("str_key", "str_value") + .put("str_arr_key", "str_value1", "str_value2") + .put("bool_key", true) + .put("bool_arr_key", true, false) + .put("double_key", 1.1) + .put("double_arr_key", 1.1, 2.2) + .put("int_key", 1) + .put("int_arr_key", 1, 2) + .put( + "kv_list_key", + ExtendedAttributes.builder() + .put("bool_key", true) + .put("double_key", 1.1) + .put("int_key", 1) + .put( + "kv_list_key", + ExtendedAttributes.builder() + .put("str_key", "str_value") + .build()) + .put("str_key", "str_value") + .build()) + .build()) + .build())); + + assertThat(logRecord.getTraceId().toByteArray()).isEqualTo(TRACE_ID_BYTES); + assertThat(logRecord.getSpanId().toByteArray()).isEqualTo(SPAN_ID_BYTES); + assertThat(logRecord.getSeverityText()).isEqualTo("INFO"); + + assertThat(logRecord.getBody()).isEqualTo(AnyValue.newBuilder().setStringValue(BODY).build()); + assertThat(logRecord.getDroppedAttributesCount()).isEqualTo(1); + assertThat(logRecord.getTimeUnixNano()).isEqualTo(12345); + assertThat(logRecord.getObservedTimeUnixNano()).isEqualTo(6789); + assertThat(logRecord.getEventName()).isEqualTo(EVENT_NAME); + assertThat(logRecord.getAttributesList()) + .containsExactlyInAnyOrder( + keyValue("str_key", anyValue("str_value")), + keyValue( + "str_arr_key", + anyValue(Arrays.asList(anyValue("str_value1"), anyValue("str_value2")))), + keyValue("bool_key", anyValue(true)), + keyValue("bool_arr_key", anyValue(Arrays.asList(anyValue(true), anyValue(false)))), + keyValue("double_key", anyValue(1.1)), + keyValue("double_arr_key", anyValue(Arrays.asList(anyValue(1.1), anyValue(2.2)))), + keyValue("int_key", anyValue(1)), + keyValue("int_arr_key", anyValue(Arrays.asList(anyValue(1), anyValue(2)))), + keyValue( + "kv_list_key", + AnyValue.newBuilder() + .setKvlistValue( + KeyValueList.newBuilder() + .addValues(keyValue("bool_key", anyValue(true))) + .addValues(keyValue("double_key", anyValue(1.1))) + .addValues(keyValue("int_key", anyValue(1))) + .addValues( + keyValue( + "kv_list_key", + AnyValue.newBuilder() + .setKvlistValue( + KeyValueList.newBuilder() + .addValues( + keyValue("str_key", anyValue("str_value"))) + .build()) + .build())) + .addValues(keyValue("str_key", anyValue("str_value"))) + .build()) + .build())); + } + + private static AnyValue anyValue(String value) { + return AnyValue.newBuilder().setStringValue(value).build(); + } + + private static AnyValue anyValue(long value) { + return AnyValue.newBuilder().setIntValue(value).build(); + } + + private static AnyValue anyValue(double value) { + return AnyValue.newBuilder().setDoubleValue(value).build(); + } + + private static AnyValue anyValue(boolean value) { + return AnyValue.newBuilder().setBoolValue(value).build(); + } + + private static AnyValue anyValue(List value) { + return AnyValue.newBuilder() + .setArrayValue(ArrayValue.newBuilder().addAllValues(value).build()) + .build(); + } + + private static KeyValue keyValue(String key, AnyValue anyValue) { + return KeyValue.newBuilder().setKey(key).setValue(anyValue).build(); + } + + @SuppressWarnings("unchecked") + private static T parse(T prototype, Marshaler marshaler) { + byte[] serialized = toByteArray(marshaler); + T result; + try { + result = (T) prototype.newBuilderForType().mergeFrom(serialized).build(); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + // Our marshaler should produce the exact same length of serialized output (for example, field + // default values are not outputted), so we check that here. The output itself may have slightly + // different ordering, mostly due to the way we don't output oneof values in field order all the + // tieme. If the lengths are equal and the resulting protos are equal, the marshaling is + // guaranteed to be valid. + assertThat(result.getSerializedSize()).isEqualTo(serialized.length); + + // We don't compare JSON strings due to some differences (particularly serializing enums as + // numbers instead of names). This may improve in the future but what matters is what we produce + // can be parsed. + Message.Builder builder = prototype.newBuilderForType(); + try { + String json = toJson(marshaler); + JsonFormat.parser().merge(json, builder); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + + // Hackily swap out "hex as base64" decoded IDs with correct ones since no JSON protobuf + // libraries currently support customizing on the parse side. + if (result instanceof LogRecord) { + fixSpanJsonIds((LogRecord.Builder) builder); + } + + if (result instanceof ResourceLogs) { + ResourceLogs.Builder fixed = (ResourceLogs.Builder) builder; + for (ScopeLogs.Builder ill : fixed.getScopeLogsBuilderList()) { + for (LogRecord.Builder span : ill.getLogRecordsBuilderList()) { + fixSpanJsonIds(span); + } + } + } + + assertThat(builder.build()).isEqualTo(result); + + return result; + } + + private static void fixSpanJsonIds(LogRecord.Builder span) { + span.setTraceId(toHex(span.getTraceId())); + span.setSpanId(toHex(span.getSpanId())); + } + + @SuppressWarnings("UnusedMethod") + private static ByteString toHex(ByteString hexReadAsBase64) { + String hex = + Base64.getEncoder().encodeToString(hexReadAsBase64.toByteArray()).toLowerCase(Locale.ROOT); + return ByteString.copyFrom(OtelEncodingUtils.bytesFromBase16(hex, hex.length())); + } + + private static byte[] toByteArray(Marshaler marshaler) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeBinaryTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return bos.toByteArray(); + } + + private static String toJson(Marshaler marshaler) { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeJsonTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new String(bos.toByteArray(), StandardCharsets.UTF_8); + } + + private static Marshaler createMarshaler(StatelessMarshaler marshaler, T data) { + return new Marshaler() { + private final MarshalerContext context = new MarshalerContext(); + private final int size = marshaler.getBinarySerializedSize(data, context); + + @Override + public int getBinarySerializedSize() { + return size; + } + + @Override + protected void writeTo(Serializer output) throws IOException { + context.resetReadIndex(); + marshaler.writeTo(output, data, context); + } + }; + } + + private enum MarshalerSource { + MARSHALER { + @Override + Marshaler create(LogRecordData logData) { + return LogMarshaler.create(logData); + } + }, + LOW_ALLOCATION_MARSHALER { + @Override + Marshaler create(LogRecordData logData) { + return createMarshaler(LogStatelessMarshaler.INSTANCE, logData); + } + }; + + abstract Marshaler create(LogRecordData logData); + } +} diff --git a/sdk/common/build.gradle.kts b/sdk/common/build.gradle.kts index e14ce596d8c..f369ae1e84f 100644 --- a/sdk/common/build.gradle.kts +++ b/sdk/common/build.gradle.kts @@ -12,6 +12,7 @@ otelJava.moduleName.set("io.opentelemetry.sdk.common") dependencies { api(project(":api:all")) + compileOnly(project(":api:incubator")) annotationProcessor("com.google.auto.value:auto-value") diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java index 9616eb75fcf..7a09093a315 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java @@ -66,7 +66,7 @@ public Object put(AttributeKey key, Object value) { return super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); } - /** Get the total number of attributes added, including those dropped for capcity limits. */ + /** Get the total number of attributes added, including those dropped for capacity limits. */ public int getTotalAddedValues() { return totalAddedValues; } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java new file mode 100644 index 00000000000..440e472666e --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributesBuilder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** + * A map with a fixed capacity that drops attributes when the map gets full, and which truncates + * string and array string attribute values to the {@link #lengthLimit}. + * + *

{@link ExtendedAttributes} analog of {@link AttributesMap}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ExtendedAttributesMap extends HashMap, Object> + implements ExtendedAttributes { + + private static final long serialVersionUID = -2674974862318200501L; + + private final long capacity; + private final int lengthLimit; + private int totalAddedValues = 0; + + private ExtendedAttributesMap(long capacity, int lengthLimit) { + this.capacity = capacity; + this.lengthLimit = lengthLimit; + } + + /** + * Create an instance. + * + * @param capacity the max number of extended attribute entries + * @param lengthLimit the maximum length of string attributes + */ + public static ExtendedAttributesMap create(long capacity, int lengthLimit) { + return new ExtendedAttributesMap(capacity, lengthLimit); + } + + /** Add the attribute key value pair, applying capacity and length limits. */ + public void put(ExtendedAttributeKey key, T value) { + totalAddedValues++; + // TODO(jack-berg): apply capcity to nested entries + if (size() >= capacity && !containsKey(key)) { + return; + } + // TODO(jack-berg): apply limits to nested entries + super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); + } + + /** + * Get the total number of extended attributes added, including those dropped for capacity limits. + */ + public int getTotalAddedValues() { + return totalAddedValues; + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public T get(ExtendedAttributeKey key) { + return (T) super.get(key); + } + + @Override + public Map, Object> asMap() { + // Because ExtendedAttributes is marked Immutable, IDEs may recognize this as redundant usage. + // However, this class is private and is actually mutable, so we need to wrap with + // unmodifiableMap anyways. We implement the immutable ExtendedAttributes for this class to + // support the ExtendedAttributes.builder().putAll usage - it is tricky but an implementation + // detail of this private class. + return Collections.unmodifiableMap(this); + } + + @Override + public ExtendedAttributesBuilder toBuilder() { + return ExtendedAttributes.builder().putAll(this); + } + + @Override + public void forEach(BiConsumer, ? super Object> action) { + // https://github.com/open-telemetry/opentelemetry-java/issues/4161 + // Help out android desugaring by having an explicit call to HashMap.forEach, when forEach is + // just called through ExtendedAttributes.forEach desugaring is unable to correctly handle it. + super.forEach(action); + } + + @Override + public Attributes asAttributes() { + return immutableCopy().asAttributes(); + } + + @Override + public String toString() { + return "ExtendedAttributesMap{" + + "data=" + + super.toString() + + ", capacity=" + + capacity + + ", totalAddedValues=" + + totalAddedValues + + '}'; + } + + /** Create an immutable copy of the extended attributes in this map. */ + public ExtendedAttributes immutableCopy() { + return ExtendedAttributes.builder().putAll(this).build(); + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java index 97f337cc194..2e4b4b5e40d 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java @@ -7,17 +7,25 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.AttributeUtil; +import io.opentelemetry.sdk.internal.ExtendedAttributesMap; import java.time.Instant; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; /** SDK implementation of {@link ExtendedLogRecordBuilder}. */ final class ExtendedSdkLogRecordBuilder extends SdkLogRecordBuilder implements ExtendedLogRecordBuilder { + @Nullable private String eventName; + @Nullable private ExtendedAttributesMap extendedAttributes; + ExtendedSdkLogRecordBuilder( LoggerSharedState loggerSharedState, InstrumentationScopeInfo instrumentationScopeInfo) { super(loggerSharedState, instrumentationScopeInfo); @@ -25,13 +33,18 @@ final class ExtendedSdkLogRecordBuilder extends SdkLogRecordBuilder @Override public ExtendedSdkLogRecordBuilder setEventName(String eventName) { - super.setEventName(eventName); + this.eventName = eventName; return this; } @Override public ExtendedSdkLogRecordBuilder setException(Throwable throwable) { - super.setException(throwable); + if (throwable == null) { + return this; + } + + AttributeUtil.addExceptionAttributes(throwable, this::setAttribute); + return this; } @@ -90,8 +103,52 @@ public ExtendedSdkLogRecordBuilder setBody(Value value) { } @Override - public ExtendedSdkLogRecordBuilder setAttribute(AttributeKey key, T value) { - super.setAttribute(key, value); + public ExtendedSdkLogRecordBuilder setAttribute(ExtendedAttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + if (this.extendedAttributes == null) { + this.extendedAttributes = + ExtendedAttributesMap.create( + logLimits.getMaxNumberOfAttributes(), logLimits.getMaxAttributeValueLength()); + } + this.extendedAttributes.put(key, value); return this; } + + @Override + public ExtendedSdkLogRecordBuilder setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + return setAttribute(ExtendedAttributeKey.fromAttributeKey(key), value); + } + + @Override + public void emit() { + if (loggerSharedState.hasBeenShutdown()) { + return; + } + Context context = this.context == null ? Context.current() : this.context; + long observedTimestampEpochNanos = + this.observedTimestampEpochNanos == 0 + ? this.loggerSharedState.getClock().now() + : this.observedTimestampEpochNanos; + loggerSharedState + .getLogRecordProcessor() + .onEmit( + context, + ExtendedSdkReadWriteLogRecord.create( + loggerSharedState.getLogLimits(), + loggerSharedState.getResource(), + instrumentationScopeInfo, + eventName, + timestampEpochNanos, + observedTimestampEpochNanos, + Span.fromContext(context).getSpanContext(), + severity, + severityText, + body, + extendedAttributes)); + } } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordData.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordData.java new file mode 100644 index 00000000000..495f826624d --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordData.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +abstract class ExtendedSdkLogRecordData implements ExtendedLogRecordData { + + ExtendedSdkLogRecordData() {} + + static ExtendedSdkLogRecordData create( + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + @Nullable String eventName, + long epochNanos, + long observedEpochNanos, + SpanContext spanContext, + Severity severity, + @Nullable String severityText, + @Nullable Value body, + ExtendedAttributes attributes, + int totalAttributeCount) { + return new AutoValue_ExtendedSdkLogRecordData( + resource, + instrumentationScopeInfo, + epochNanos, + observedEpochNanos, + spanContext, + severity, + severityText, + totalAttributeCount, + eventName, + attributes, + body); + } + + @Override + @Nullable + public abstract Value getBodyValue(); + + @Override + @SuppressWarnings("deprecation") // Implementation of deprecated method + public io.opentelemetry.sdk.logs.data.Body getBody() { + Value valueBody = getBodyValue(); + return valueBody == null + ? io.opentelemetry.sdk.logs.data.Body.empty() + : io.opentelemetry.sdk.logs.data.Body.string(valueBody.asString()); + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkReadWriteLogRecord.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkReadWriteLogRecord.java new file mode 100644 index 00000000000..ef3789d44dd --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkReadWriteLogRecord.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ExtendedAttributesMap; +import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; +import io.opentelemetry.sdk.logs.internal.ExtendedReadWriteLogRecord; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +class ExtendedSdkReadWriteLogRecord extends SdkReadWriteLogRecord + implements ExtendedReadWriteLogRecord { + + @Nullable private final String eventName; + private final Object lock = new Object(); + + @GuardedBy("lock") + @Nullable + private ExtendedAttributesMap extendedAttributes; + + @SuppressWarnings("unused") + private ExtendedSdkReadWriteLogRecord( + LogLimits logLimits, + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + @Nullable String eventName, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + @Nullable String severityText, + @Nullable Value body, + @Nullable ExtendedAttributesMap extendedAttributes) { + super( + logLimits, + resource, + instrumentationScopeInfo, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + null); + this.eventName = eventName; + this.extendedAttributes = extendedAttributes; + } + + /** Create the extended log record with the given configuration. */ + static ExtendedSdkReadWriteLogRecord create( + LogLimits logLimits, + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + @Nullable String eventName, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + @Nullable String severityText, + @Nullable Value body, + @Nullable ExtendedAttributesMap extendedAttributes) { + return new ExtendedSdkReadWriteLogRecord( + logLimits, + resource, + instrumentationScopeInfo, + eventName, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + extendedAttributes); + } + + @Override + public ExtendedSdkReadWriteLogRecord setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + return setAttribute(ExtendedAttributeKey.fromAttributeKey(key), value); + } + + @Override + public ExtendedSdkReadWriteLogRecord setAttribute(ExtendedAttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + synchronized (lock) { + if (extendedAttributes == null) { + extendedAttributes = + ExtendedAttributesMap.create( + logLimits.getMaxNumberOfAttributes(), logLimits.getMaxAttributeValueLength()); + } + extendedAttributes.put(key, value); + } + return this; + } + + private ExtendedAttributes getImmutableExtendedAttributes() { + synchronized (lock) { + if (extendedAttributes == null) { + return ExtendedAttributes.empty(); + } + return extendedAttributes.immutableCopy(); + } + } + + @Override + public ExtendedLogRecordData toLogRecordData() { + synchronized (lock) { + return ExtendedSdkLogRecordData.create( + resource, + instrumentationScopeInfo, + eventName, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + getImmutableExtendedAttributes(), + extendedAttributes == null ? 0 : extendedAttributes.getTotalAddedValues()); + } + } + + @Override + public Attributes getAttributes() { + return getExtendedAttributes().asAttributes(); + } + + @Nullable + @Override + public T getAttribute(AttributeKey key) { + return getAttribute(ExtendedAttributeKey.fromAttributeKey(key)); + } + + @Nullable + @Override + public T getAttribute(ExtendedAttributeKey key) { + synchronized (lock) { + if (extendedAttributes == null || extendedAttributes.isEmpty()) { + return null; + } + return extendedAttributes.get(key); + } + } + + @Override + public ExtendedAttributes getExtendedAttributes() { + return getImmutableExtendedAttributes(); + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java index a73f4aa681b..1a108b366c9 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java @@ -12,7 +12,6 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.internal.AttributeUtil; import io.opentelemetry.sdk.internal.AttributesMap; import java.time.Instant; import java.util.concurrent.TimeUnit; @@ -21,17 +20,16 @@ /** SDK implementation of {@link LogRecordBuilder}. */ class SdkLogRecordBuilder implements LogRecordBuilder { - private final LoggerSharedState loggerSharedState; - private final LogLimits logLimits; - - private final InstrumentationScopeInfo instrumentationScopeInfo; - @Nullable private String eventName; - private long timestampEpochNanos; - private long observedTimestampEpochNanos; - @Nullable private Context context; - private Severity severity = Severity.UNDEFINED_SEVERITY_NUMBER; - @Nullable private String severityText; - @Nullable private Value body; + protected final LoggerSharedState loggerSharedState; + protected final LogLimits logLimits; + + protected final InstrumentationScopeInfo instrumentationScopeInfo; + protected long timestampEpochNanos; + protected long observedTimestampEpochNanos; + @Nullable protected Context context; + protected Severity severity = Severity.UNDEFINED_SEVERITY_NUMBER; + @Nullable protected String severityText; + @Nullable protected Value body; @Nullable private AttributesMap attributes; SdkLogRecordBuilder( @@ -41,23 +39,6 @@ class SdkLogRecordBuilder implements LogRecordBuilder { this.instrumentationScopeInfo = instrumentationScopeInfo; } - // accessible via ExtendedSdkLogRecordBuilder - SdkLogRecordBuilder setEventName(String eventName) { - this.eventName = eventName; - return this; - } - - // accessible via ExtendedSdkLogRecordBuilder - SdkLogRecordBuilder setException(Throwable throwable) { - if (throwable == null) { - return this; - } - - AttributeUtil.addExceptionAttributes(throwable, this::setAttribute); - - return this; - } - @Override public SdkLogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { this.timestampEpochNanos = unit.toNanos(timestamp); @@ -145,7 +126,6 @@ public void emit() { loggerSharedState.getLogLimits(), loggerSharedState.getResource(), instrumentationScopeInfo, - eventName, timestampEpochNanos, observedTimestampEpochNanos, Span.fromContext(context).getSpanContext(), diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordData.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordData.java index eb8e966a7fd..1927a0ec572 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordData.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordData.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; +import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -19,14 +19,13 @@ @AutoValue @AutoValue.CopyAnnotations @Immutable -abstract class SdkLogRecordData implements ExtendedLogRecordData { +abstract class SdkLogRecordData implements LogRecordData { SdkLogRecordData() {} static SdkLogRecordData create( Resource resource, InstrumentationScopeInfo instrumentationScopeInfo, - @Nullable String eventName, long epochNanos, long observedEpochNanos, SpanContext spanContext, @@ -45,18 +44,13 @@ static SdkLogRecordData create( severityText, attributes, totalAttributeCount, - body, - eventName); + body); } @Override @Nullable public abstract Value getBodyValue(); - @Override - @Nullable - public abstract String getEventName(); - @Override @SuppressWarnings("deprecation") // Implementation of deprecated method public io.opentelemetry.sdk.logs.data.Body getBody() { diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkReadWriteLogRecord.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkReadWriteLogRecord.java index ad6197d8cf3..8b73a8eacb8 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkReadWriteLogRecord.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkReadWriteLogRecord.java @@ -21,27 +21,25 @@ @ThreadSafe class SdkReadWriteLogRecord implements ReadWriteLogRecord { - private final LogLimits logLimits; - private final Resource resource; - private final InstrumentationScopeInfo instrumentationScopeInfo; - @Nullable private final String eventName; - private final long timestampEpochNanos; - private final long observedTimestampEpochNanos; - private final SpanContext spanContext; - private final Severity severity; - @Nullable private final String severityText; - @Nullable private final Value body; + protected final LogLimits logLimits; + protected final Resource resource; + protected final InstrumentationScopeInfo instrumentationScopeInfo; + protected final long timestampEpochNanos; + protected final long observedTimestampEpochNanos; + protected final SpanContext spanContext; + protected final Severity severity; + @Nullable protected final String severityText; + @Nullable protected final Value body; private final Object lock = new Object(); @GuardedBy("lock") @Nullable private AttributesMap attributes; - private SdkReadWriteLogRecord( + protected SdkReadWriteLogRecord( LogLimits logLimits, Resource resource, InstrumentationScopeInfo instrumentationScopeInfo, - @Nullable String eventName, long timestampEpochNanos, long observedTimestampEpochNanos, SpanContext spanContext, @@ -52,7 +50,6 @@ private SdkReadWriteLogRecord( this.logLimits = logLimits; this.resource = resource; this.instrumentationScopeInfo = instrumentationScopeInfo; - this.eventName = eventName; this.timestampEpochNanos = timestampEpochNanos; this.observedTimestampEpochNanos = observedTimestampEpochNanos; this.spanContext = spanContext; @@ -67,7 +64,6 @@ static SdkReadWriteLogRecord create( LogLimits logLimits, Resource resource, InstrumentationScopeInfo instrumentationScopeInfo, - @Nullable String eventName, long timestampEpochNanos, long observedTimestampEpochNanos, SpanContext spanContext, @@ -79,7 +75,6 @@ static SdkReadWriteLogRecord create( logLimits, resource, instrumentationScopeInfo, - eventName, timestampEpochNanos, observedTimestampEpochNanos, spanContext, @@ -120,7 +115,6 @@ public LogRecordData toLogRecordData() { return SdkLogRecordData.create( resource, instrumentationScopeInfo, - eventName, timestampEpochNanos, observedTimestampEpochNanos, spanContext, diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/internal/ExtendedLogRecordData.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/internal/ExtendedLogRecordData.java index 61e48eb7821..9cfc40b4231 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/internal/ExtendedLogRecordData.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/internal/ExtendedLogRecordData.java @@ -5,6 +5,8 @@ package io.opentelemetry.sdk.logs.data.internal; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.sdk.logs.data.LogRecordData; import javax.annotation.Nullable; @@ -17,4 +19,18 @@ public interface ExtendedLogRecordData extends LogRecordData { @Nullable String getEventName(); + + /** Returns the attributes for this log, or {@link ExtendedAttributes#empty()} if unset. */ + ExtendedAttributes getExtendedAttributes(); + + /** + * Returns the attributes for this log, or {@link Attributes#empty()} if unset. + * + * @deprecated Use {@link #getExtendedAttributes()}. + */ + @Override + @Deprecated + default Attributes getAttributes() { + return getExtendedAttributes().asAttributes(); + } } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/ExtendedReadWriteLogRecord.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/ExtendedReadWriteLogRecord.java new file mode 100644 index 00000000000..527c1e14063 --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/ExtendedReadWriteLogRecord.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs.internal; + +import io.opentelemetry.api.incubator.common.ExtendedAttributeKey; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; +import io.opentelemetry.sdk.logs.ReadWriteLogRecord; +import io.opentelemetry.sdk.logs.data.internal.ExtendedLogRecordData; +import javax.annotation.Nullable; + +/** + * A collection of configuration options which define the behavior of a {@link + * io.opentelemetry.api.logs.Logger}. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public interface ExtendedReadWriteLogRecord extends ReadWriteLogRecord { + + /** + * Sets an attribute on the log record. If the log record previously contained a mapping for the + * key, the old value is replaced by the specified value. + * + *

Note: the behavior of null values is undefined, and hence strongly discouraged. + */ + ExtendedReadWriteLogRecord setAttribute(ExtendedAttributeKey key, T value); + + /** + * Sets attributes to the {@link ReadWriteLogRecord}. If the {@link ReadWriteLogRecord} previously + * contained a mapping for any of the keys, the old values are replaced by the specified values. + * + * @param extendedAttributes the attributes + * @return this. + */ + @SuppressWarnings("unchecked") + default ExtendedReadWriteLogRecord setAllAttributes(ExtendedAttributes extendedAttributes) { + if (extendedAttributes == null || extendedAttributes.isEmpty()) { + return this; + } + extendedAttributes.forEach( + (attributeKey, value) -> + this.setAttribute((ExtendedAttributeKey) attributeKey, value)); + return this; + } + + /** Return an immutable {@link ExtendedLogRecordData} instance representing this log record. */ + @Override + ExtendedLogRecordData toLogRecordData(); + + /** + * Returns the value of a given attribute if it exists. This is the equivalent of calling + * getAttributes().get(key) + */ + @Nullable + T getAttribute(ExtendedAttributeKey key); + + /** Returns the attributes for this log, or {@link ExtendedAttributes#empty()} if unset. */ + ExtendedAttributes getExtendedAttributes(); +} diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ReadWriteLogRecordTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ReadWriteLogRecordTest.java index 95215ee57db..7a444817d40 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ReadWriteLogRecordTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/ReadWriteLogRecordTest.java @@ -62,7 +62,6 @@ SdkReadWriteLogRecord buildLogRecord() { limits, resource, scope, - "event name", 0L, 0L, spanContext, diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilderTest.java index 202f73917f1..c6f947bc062 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilderTest.java @@ -65,7 +65,6 @@ void emit_AllFields() { Instant timestamp = Instant.now(); Instant observedTimestamp = Instant.now().plusNanos(100); - String eventName = "event name"; String bodyStr = "body"; String sevText = "sevText"; Severity severity = Severity.DEBUG3; @@ -76,7 +75,6 @@ void emit_AllFields() { TraceFlags.getSampled(), TraceState.getDefault()); - builder.setEventName(eventName); builder.setBody(bodyStr); builder.setTimestamp(123, TimeUnit.SECONDS); builder.setTimestamp(timestamp); diff --git a/sdk/testing/build.gradle.kts b/sdk/testing/build.gradle.kts index e6a095179d5..44bbcad90d8 100644 --- a/sdk/testing/build.gradle.kts +++ b/sdk/testing/build.gradle.kts @@ -9,6 +9,7 @@ otelJava.moduleName.set("io.opentelemetry.sdk.testing") dependencies { api(project(":api:all")) api(project(":sdk:all")) + compileOnly(project(":api:incubator")) compileOnly("org.assertj:assertj-core") compileOnly("junit:junit") @@ -21,3 +22,13 @@ dependencies { testImplementation("junit:junit") testImplementation("org.junit.vintage:junit-vintage-engine") } + +testing { + suites { + register("testIncubating") { + dependencies { + implementation(project(":api:incubator")) + } + } + } +} diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/logs/internal/TestExtendedLogRecordData.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/logs/internal/TestExtendedLogRecordData.java index 750344bf748..10c69da9cdb 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/logs/internal/TestExtendedLogRecordData.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/logs/internal/TestExtendedLogRecordData.java @@ -8,6 +8,7 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.incubator.common.ExtendedAttributes; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; @@ -183,9 +184,14 @@ public Builder setBody(io.opentelemetry.sdk.logs.data.Body body) { public abstract Builder setBodyValue(@Nullable Value body); /** Set the attributes. */ - public abstract Builder setAttributes(Attributes attributes); + public Builder setAttributes(Attributes attributes) { + return setExtendedAttributes(ExtendedAttributes.builder().putAll(attributes).build()); + } /** Set the total attribute count. */ public abstract Builder setTotalAttributeCount(int totalAttributeCount); + + /** Set extended attributes. * */ + public abstract Builder setExtendedAttributes(ExtendedAttributes extendedAttributes); } }