From efa46a5dccb962b50943a0b4905c1800282d6be6 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:05:36 -0500 Subject: [PATCH] Experimental support for Log AnyValue body (#5880) --- .../internal/otlp/logs/LogMarshaler.java | 2 +- extensions/incubator/build.gradle.kts | 2 + .../extension/incubator/logs/AnyValue.java | 106 +++++++++ .../incubator/logs/AnyValueArray.java | 63 +++++ .../incubator/logs/AnyValueBoolean.java | 54 +++++ .../incubator/logs/AnyValueBytes.java | 62 +++++ .../incubator/logs/AnyValueDouble.java | 54 +++++ .../incubator/logs/AnyValueLong.java | 54 +++++ .../incubator/logs/AnyValueString.java | 55 +++++ .../incubator/logs/AnyValueType.java | 21 ++ .../logs/ExtendedLogRecordBuilder.java | 15 ++ .../extension/incubator/logs/KeyAnyValue.java | 25 ++ .../incubator/logs/KeyAnyValueImpl.java | 18 ++ .../incubator/logs/KeyAnyValueList.java | 75 ++++++ .../incubator/logs/AnyValueTest.java | 223 ++++++++++++++++++ sdk/logs/build.gradle.kts | 2 + .../sdk/logs/SdkLogRecordBuilder.java | 11 +- .../io/opentelemetry/sdk/logs/data/Body.java | 2 + .../sdk/logs/internal/AnyValueBody.java | 43 ++++ .../sdk/logs/AnyValueBodyTest.java | 156 ++++++++++++ 20 files changed, 1041 insertions(+), 2 deletions(-) create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValue.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueArray.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBoolean.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBytes.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueDouble.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueLong.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueString.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueType.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/ExtendedLogRecordBuilder.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValue.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueImpl.java create mode 100644 extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueList.java create mode 100644 extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/logs/AnyValueTest.java create mode 100644 sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/AnyValueBody.java create mode 100644 sdk/logs/src/test/java/io/opentelemetry/sdk/logs/AnyValueBodyTest.java 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 bd2c6b3b8a2..7a68041e5d1 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 @@ -41,7 +41,7 @@ static LogMarshaler create(LogRecordData logRecordData) { KeyValueMarshaler[] attributeMarshalers = KeyValueMarshaler.createRepeated(logRecordData.getAttributes()); - // For now, map all the bodies to String AnyValue. + // TODO(jack-berg): handle AnyValue log body StringAnyValueMarshaler anyValueMarshaler = new StringAnyValueMarshaler(MarshalerUtil.toBytes(logRecordData.getBody().asString())); diff --git a/extensions/incubator/build.gradle.kts b/extensions/incubator/build.gradle.kts index 6acaf6cddb2..6e79f9361fd 100644 --- a/extensions/incubator/build.gradle.kts +++ b/extensions/incubator/build.gradle.kts @@ -12,5 +12,7 @@ otelJava.moduleName.set("io.opentelemetry.extension.incubator") dependencies { api(project(":api:all")) + annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":sdk:testing")) } diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValue.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValue.java new file mode 100644 index 00000000000..6f250e10da7 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValue.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +/** + * AnyValue mirrors the proto AnyValue + * message type, and is used to model any type. + * + *

It can be used to represent: + * + *

+ * + * @param the type. See {@link #getValue()} for description of types. + */ +public interface AnyValue { + + /** Returns an {@link AnyValue} for the {@link String} value. */ + static AnyValue of(String value) { + return AnyValueString.create(value); + } + + /** Returns an {@link AnyValue} for the {@code boolean} value. */ + static AnyValue of(boolean value) { + return AnyValueBoolean.create(value); + } + + /** Returns an {@link AnyValue} for the {@code long} value. */ + static AnyValue of(long value) { + return AnyValueLong.create(value); + } + + /** Returns an {@link AnyValue} for the {@code double} value. */ + static AnyValue of(double value) { + return AnyValueDouble.create(value); + } + + /** Returns an {@link AnyValue} for the {@code byte[]} value. */ + static AnyValue of(byte[] value) { + return AnyValueBytes.create(value); + } + + /** Returns an {@link AnyValue} for the array of {@link AnyValue} values. */ + static AnyValue>> of(AnyValue... value) { + return AnyValueArray.create(value); + } + + /** + * Returns an {@link AnyValue} for the array of {@link KeyAnyValue} values. {@link + * KeyAnyValue#getKey()} values should not repeat - duplicates may be dropped. + */ + static AnyValue> of(KeyAnyValue... value) { + return KeyAnyValueList.create(value); + } + + /** Returns an {@link AnyValue} for the {@link Map} of key, {@link AnyValue}. */ + static AnyValue> of(Map> value) { + return KeyAnyValueList.createFromMap(value); + } + + /** Returns the type of this {@link AnyValue}. Useful for building switch statements. */ + AnyValueType getType(); + + /** + * Returns the value for this {@link AnyValue}. + * + *

The return type varies by {@link #getType()} as described below: + * + *

    + *
  • {@link AnyValueType#STRING} returns {@link String} + *
  • {@link AnyValueType#BOOLEAN} returns {@code boolean} + *
  • {@link AnyValueType#LONG} returns {@code long} + *
  • {@link AnyValueType#DOUBLE} returns {@code double} + *
  • {@link AnyValueType#ARRAY} returns {@link List} of {@link AnyValue} + *
  • {@link AnyValueType#KEY_VALUE_LIST} returns {@link List} of {@link KeyAnyValue} + *
  • {@link AnyValueType#BYTES} returns read only {@link ByteBuffer}. See {@link + * ByteBuffer#asReadOnlyBuffer()}. + *
+ */ + T getValue(); + + /** + * Return a string encoding of this {@link AnyValue}. This is intended to be a fallback serialized + * representation in case there is no suitable encoding that can utilize {@link #getType()} / + * {@link #getValue()} to serialize specific types. + */ + // TODO(jack-berg): Should this be a JSON encoding? + String asString(); +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueArray.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueArray.java new file mode 100644 index 00000000000..dd96a60793d --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueArray.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import static java.util.stream.Collectors.joining; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +final class AnyValueArray implements AnyValue>> { + + private final List> value; + + private AnyValueArray(List> value) { + this.value = value; + } + + static AnyValue>> create(AnyValue... value) { + Objects.requireNonNull(value, "value must not be null"); + List> list = new ArrayList<>(value.length); + list.addAll(Arrays.asList(value)); + return new AnyValueArray(Collections.unmodifiableList(list)); + } + + @Override + public AnyValueType getType() { + return AnyValueType.ARRAY; + } + + @Override + public List> getValue() { + return value; + } + + @Override + public String asString() { + return value.stream().map(AnyValue::asString).collect(joining(", ", "[", "]")); + } + + @Override + public String toString() { + return "AnyValueArray{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBoolean.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBoolean.java new file mode 100644 index 00000000000..5fa862b777f --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBoolean.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import java.util.Objects; + +final class AnyValueBoolean implements AnyValue { + + private final boolean value; + + private AnyValueBoolean(boolean value) { + this.value = value; + } + + static AnyValue create(boolean value) { + return new AnyValueBoolean(value); + } + + @Override + public AnyValueType getType() { + return AnyValueType.BOOLEAN; + } + + @Override + public Boolean getValue() { + return value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public String toString() { + return "AnyValueBoolean{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return Boolean.hashCode(value); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBytes.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBytes.java new file mode 100644 index 00000000000..1677a3313e8 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueBytes.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; + +final class AnyValueBytes implements AnyValue { + + private final byte[] raw; + + private AnyValueBytes(byte[] value) { + this.raw = value; + } + + static AnyValue create(byte[] value) { + Objects.requireNonNull(value, "value must not be null"); + return new AnyValueBytes(Arrays.copyOf(value, value.length)); + } + + @Override + public AnyValueType getType() { + return AnyValueType.BYTES; + } + + @Override + public ByteBuffer getValue() { + return ByteBuffer.wrap(raw).asReadOnlyBuffer(); + } + + @Override + public String asString() { + // TODO: base64 would be better, but isn't available in android and java. Can we vendor in a + // base64 implementation? + char[] arr = new char[raw.length * 2]; + OtelEncodingUtils.bytesToBase16(raw, arr, raw.length); + return new String(arr); + } + + @Override + public String toString() { + return "AnyValueBytes{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValueBytes) && Arrays.equals(this.raw, ((AnyValueBytes) o).raw); + } + + @Override + public int hashCode() { + return Arrays.hashCode(raw); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueDouble.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueDouble.java new file mode 100644 index 00000000000..4e2cdccf33b --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueDouble.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import java.util.Objects; + +final class AnyValueDouble implements AnyValue { + + private final double value; + + private AnyValueDouble(double value) { + this.value = value; + } + + static AnyValue create(double value) { + return new AnyValueDouble(value); + } + + @Override + public AnyValueType getType() { + return AnyValueType.DOUBLE; + } + + @Override + public Double getValue() { + return value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public String toString() { + return "AnyValueDouble{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return Double.hashCode(value); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueLong.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueLong.java new file mode 100644 index 00000000000..558a08376ee --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueLong.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import java.util.Objects; + +final class AnyValueLong implements AnyValue { + + private final long value; + + private AnyValueLong(long value) { + this.value = value; + } + + static AnyValue create(long value) { + return new AnyValueLong(value); + } + + @Override + public AnyValueType getType() { + return AnyValueType.LONG; + } + + @Override + public Long getValue() { + return value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public String toString() { + return "AnyValueLong{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return Long.hashCode(value); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueString.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueString.java new file mode 100644 index 00000000000..6a7b0a1c8e2 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueString.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import java.util.Objects; + +final class AnyValueString implements AnyValue { + + private final String value; + + private AnyValueString(String value) { + this.value = value; + } + + static AnyValue create(String value) { + Objects.requireNonNull(value, "value must not be null"); + return new AnyValueString(value); + } + + @Override + public AnyValueType getType() { + return AnyValueType.STRING; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String asString() { + return value; + } + + @Override + public String toString() { + return "AnyValueString{" + value + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueType.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueType.java new file mode 100644 index 00000000000..f683cc61ea5 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/AnyValueType.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +/** + * AnyValue type options, mirroring AnyValue#value + * options. + */ +public enum AnyValueType { + STRING, + BOOLEAN, + LONG, + DOUBLE, + ARRAY, + KEY_VALUE_LIST, + BYTES +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/ExtendedLogRecordBuilder.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/ExtendedLogRecordBuilder.java new file mode 100644 index 00000000000..b1ca789c1f6 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/ExtendedLogRecordBuilder.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import io.opentelemetry.api.logs.LogRecordBuilder; + +/** Extended {@link LogRecordBuilder} with experimental APIs. */ +public interface ExtendedLogRecordBuilder extends LogRecordBuilder { + + /** Set the body {@link AnyValue}. */ + LogRecordBuilder setBody(AnyValue body); +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValue.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValue.java new file mode 100644 index 00000000000..6aeb5eab6ab --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValue.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +/** + * Key-value pair of {@link String} key and {@link AnyValue} value. + * + * @see AnyValue#of(KeyAnyValue...) + */ +public interface KeyAnyValue { + + /** Returns a {@link KeyAnyValue} for the given {@code key} and {@code value}. */ + static KeyAnyValue of(String key, AnyValue value) { + return KeyAnyValueImpl.create(key, value); + } + + /** Returns the key. */ + String getKey(); + + /** Returns the value. */ + AnyValue getAnyValue(); +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueImpl.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueImpl.java new file mode 100644 index 00000000000..516f46e0840 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueImpl.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import com.google.auto.value.AutoValue; + +@AutoValue +abstract class KeyAnyValueImpl implements KeyAnyValue { + + KeyAnyValueImpl() {} + + static KeyAnyValueImpl create(String key, AnyValue value) { + return new AutoValue_KeyAnyValueImpl(key, value); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueList.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueList.java new file mode 100644 index 00000000000..427ed4cb13a --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/logs/KeyAnyValueList.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import static java.util.stream.Collectors.joining; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class KeyAnyValueList implements AnyValue> { + + private final List value; + + private KeyAnyValueList(List value) { + this.value = value; + } + + static AnyValue> create(KeyAnyValue... value) { + Objects.requireNonNull(value, "value must not be null"); + List list = new ArrayList<>(value.length); + list.addAll(Arrays.asList(value)); + return new KeyAnyValueList(Collections.unmodifiableList(list)); + } + + static AnyValue> createFromMap(Map> value) { + Objects.requireNonNull(value, "value must not be null"); + KeyAnyValue[] array = + value.entrySet().stream() + .map(entry -> KeyAnyValue.of(entry.getKey(), entry.getValue())) + .toArray(KeyAnyValue[]::new); + return create(array); + } + + @Override + public AnyValueType getType() { + return AnyValueType.KEY_VALUE_LIST; + } + + @Override + public List getValue() { + return value; + } + + @Override + public String asString() { + return value.stream() + .map(item -> item.getKey() + "=" + item.getAnyValue().asString()) + .collect(joining(", ", "[", "]")); + } + + @Override + public String toString() { + return "KeyAnyValueList{" + asString() + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return (o instanceof AnyValue) && Objects.equals(this.value, ((AnyValue) o).getValue()); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/logs/AnyValueTest.java b/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/logs/AnyValueTest.java new file mode 100644 index 00000000000..842cd505c61 --- /dev/null +++ b/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/logs/AnyValueTest.java @@ -0,0 +1,223 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.logs; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class AnyValueTest { + + @Test + void anyValue_OfString() { + assertThat(AnyValue.of("foo")) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.STRING); + assertThat(anyValue.getValue()).isEqualTo("foo"); + assertThat(anyValue).hasSameHashCodeAs(AnyValue.of("foo")); + }); + } + + @Test + void anyValue_OfBoolean() { + assertThat(AnyValue.of(true)) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.BOOLEAN); + assertThat(anyValue.getValue()).isEqualTo(true); + assertThat(anyValue).hasSameHashCodeAs(AnyValue.of(true)); + }); + } + + @Test + void anyValue_OfLong() { + assertThat(AnyValue.of(1L)) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.LONG); + assertThat(anyValue.getValue()).isEqualTo(1L); + assertThat(anyValue).hasSameHashCodeAs(AnyValue.of(1L)); + }); + } + + @Test + void anyValue_OfDouble() { + assertThat(AnyValue.of(1.1)) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.DOUBLE); + assertThat(anyValue.getValue()).isEqualTo(1.1); + assertThat(anyValue).hasSameHashCodeAs(AnyValue.of(1.1)); + }); + } + + @Test + void anyValue_OfByteArray() { + assertThat(AnyValue.of(new byte[] {'a', 'b'})) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.BYTES); + ByteBuffer value = anyValue.getValue(); + // AnyValueBytes returns read only view of ByteBuffer + assertThatThrownBy(value::array).isInstanceOf(ReadOnlyBufferException.class); + byte[] bytes = new byte[value.remaining()]; + value.get(bytes); + assertThat(bytes).isEqualTo(new byte[] {'a', 'b'}); + assertThat(anyValue).hasSameHashCodeAs(AnyValue.of(new byte[] {'a', 'b'})); + }); + } + + @Test + void anyValue_OfAnyValueArray() { + assertThat(AnyValue.of(AnyValue.of(true), AnyValue.of(1L))) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.ARRAY); + assertThat(anyValue.getValue()) + .isEqualTo(Arrays.asList(AnyValue.of(true), AnyValue.of(1L))); + assertThat(anyValue) + .hasSameHashCodeAs(AnyValue.of(AnyValue.of(true), AnyValue.of(1L))); + }); + } + + @Test + @SuppressWarnings("DoubleBraceInitialization") + void anyValue_OfKeyValueList() { + assertThat( + AnyValue.of( + KeyAnyValue.of("bool", AnyValue.of(true)), KeyAnyValue.of("long", AnyValue.of(1L)))) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.KEY_VALUE_LIST); + assertThat(anyValue.getValue()) + .isEqualTo( + Arrays.asList( + KeyAnyValue.of("bool", AnyValue.of(true)), + KeyAnyValue.of("long", AnyValue.of(1L)))); + assertThat(anyValue) + .hasSameHashCodeAs( + AnyValue.of( + KeyAnyValue.of("bool", AnyValue.of(true)), + KeyAnyValue.of("long", AnyValue.of(1L)))); + }); + + assertThat( + AnyValue.of( + new LinkedHashMap>() { + { + put("bool", AnyValue.of(true)); + put("long", AnyValue.of(1L)); + } + })) + .satisfies( + anyValue -> { + assertThat(anyValue.getType()).isEqualTo(AnyValueType.KEY_VALUE_LIST); + assertThat(anyValue.getValue()) + .isEqualTo( + Arrays.asList( + KeyAnyValue.of("bool", AnyValue.of(true)), + KeyAnyValue.of("long", AnyValue.of(1L)))); + assertThat(anyValue) + .hasSameHashCodeAs( + AnyValue.of( + new LinkedHashMap>() { + { + put("bool", AnyValue.of(true)); + put("long", AnyValue.of(1L)); + } + })); + }); + } + + @Test + void anyValue_NullsNotAllowed() { + assertThatThrownBy(() -> AnyValue.of((String) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + assertThatThrownBy(() -> AnyValue.of((byte[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + assertThatThrownBy(() -> AnyValue.of((AnyValue[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + assertThatThrownBy(() -> AnyValue.of((KeyAnyValue[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + assertThatThrownBy(() -> AnyValue.of((Map>) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value must not be null"); + } + + @ParameterizedTest + @MethodSource("asStringArgs") + void asString(AnyValue value, String expectedAsString) { + assertThat(value.asString()).isEqualTo(expectedAsString); + } + + @SuppressWarnings("DoubleBraceInitialization") + private static Stream asStringArgs() { + return Stream.of( + // primitives + arguments(AnyValue.of("str"), "str"), + arguments(AnyValue.of(true), "true"), + arguments(AnyValue.of(1), "1"), + arguments(AnyValue.of(1.1), "1.1"), + // heterogeneous array + arguments( + AnyValue.of(AnyValue.of("str"), AnyValue.of(true), AnyValue.of(1), AnyValue.of(1.1)), + "[str, true, 1, 1.1]"), + // key value list from KeyAnyValue array + arguments( + AnyValue.of( + KeyAnyValue.of("key1", AnyValue.of("val1")), + KeyAnyValue.of("key2", AnyValue.of(2))), + "[key1=val1, key2=2]"), + // key value list from map + arguments( + AnyValue.of( + new LinkedHashMap>() { + { + put("key1", AnyValue.of("val1")); + put("key2", AnyValue.of(2)); + } + }), + "[key1=val1, key2=2]"), + // map of map + arguments( + AnyValue.of( + Collections.singletonMap( + "child", + AnyValue.of(Collections.singletonMap("grandchild", AnyValue.of("str"))))), + "[child=[grandchild=str]]"), + // bytes + arguments( + AnyValue.of("hello world".getBytes(StandardCharsets.UTF_8)), "68656c6c6f20776f726c64")); + } + + @Test + void anyValueByteAsString() { + // TODO: add more test cases + String str = "hello world"; + String base16Encoded = AnyValue.of(str.getBytes(StandardCharsets.UTF_8)).asString(); + byte[] decodedBytes = OtelEncodingUtils.bytesFromBase16(base16Encoded, base16Encoded.length()); + assertThat(new String(decodedBytes, StandardCharsets.UTF_8)).isEqualTo(str); + } +} diff --git a/sdk/logs/build.gradle.kts b/sdk/logs/build.gradle.kts index 8ffe760b86f..6640c4ed0ee 100644 --- a/sdk/logs/build.gradle.kts +++ b/sdk/logs/build.gradle.kts @@ -12,6 +12,7 @@ otelJava.moduleName.set("io.opentelemetry.sdk.logs") dependencies { api(project(":api:all")) api(project(":sdk:common")) + implementation(project(":extensions:incubator")) implementation(project(":api:events")) @@ -20,4 +21,5 @@ dependencies { testImplementation(project(":sdk:testing")) testImplementation("org.awaitility:awaitility") + testImplementation("com.google.guava:guava") } 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 8ef8b2ae4b2..6bdd0407aa5 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 @@ -10,15 +10,18 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; +import io.opentelemetry.extension.incubator.logs.AnyValue; +import io.opentelemetry.extension.incubator.logs.ExtendedLogRecordBuilder; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.AttributesMap; import io.opentelemetry.sdk.logs.data.Body; +import io.opentelemetry.sdk.logs.internal.AnyValueBody; import java.time.Instant; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** SDK implementation of {@link LogRecordBuilder}. */ -final class SdkLogRecordBuilder implements LogRecordBuilder { +final class SdkLogRecordBuilder implements ExtendedLogRecordBuilder { private final LoggerSharedState loggerSharedState; private final LogLimits logLimits; @@ -89,6 +92,12 @@ public SdkLogRecordBuilder setBody(String body) { return this; } + @Override + public LogRecordBuilder setBody(AnyValue value) { + this.body = AnyValueBody.create(value); + return this; + } + @Override public SdkLogRecordBuilder setAttribute(AttributeKey key, T value) { if (key == null || key.getKey().isEmpty() || value == null) { diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/Body.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/Body.java index a13ecc003fe..2dc7957de91 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/Body.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/data/Body.java @@ -22,6 +22,8 @@ public interface Body { enum Type { EMPTY, STRING + // TODO (jack-berg): Add ANY_VALUE type when API for setting body to AnyValue is stable + // ANY_VALUE } /** diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/AnyValueBody.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/AnyValueBody.java new file mode 100644 index 00000000000..7a1a9f2138f --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/AnyValueBody.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs.internal; + +import io.opentelemetry.extension.incubator.logs.AnyValue; +import io.opentelemetry.sdk.logs.data.Body; +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class AnyValueBody implements Body { + + private final AnyValue value; + + private AnyValueBody(AnyValue value) { + this.value = value; + } + + public static Body create(AnyValue value) { + return new AnyValueBody(value); + } + + @Override + public Type getType() { + return Type.STRING; + } + + @Override + public String asString() { + return value.asString(); + } + + public AnyValue asAnyValue() { + return value; + } + + @Override + public String toString() { + return "AnyValueBody{" + asString() + "}"; + } +} diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/AnyValueBodyTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/AnyValueBodyTest.java new file mode 100644 index 00000000000..e793dc513d0 --- /dev/null +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/AnyValueBodyTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.extension.incubator.logs.AnyValue; +import io.opentelemetry.extension.incubator.logs.ExtendedLogRecordBuilder; +import io.opentelemetry.extension.incubator.logs.KeyAnyValue; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.logs.internal.AnyValueBody; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import org.junit.jupiter.api.Test; + +class AnyValueBodyTest { + + @Test + @SuppressWarnings("DoubleBraceInitialization") + void anyValueBody() { + InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); + SdkLoggerProvider provider = + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)) + .build(); + Logger logger = provider.get(AnyValueBodyTest.class.getName()); + + // AnyValue can be a primitive type, like a string, long, double, boolean + extendedLogRecordBuilder(logger).setBody(AnyValue.of(1)).emit(); + assertThat(exporter.getFinishedLogRecordItems()) + .hasSize(1) + .satisfiesExactly( + logRecordData -> { + // TODO (jack-berg): add assertion when ANY_VALUE is added to Body.Type + // assertThat(logRecordData.getBody().getType()).isEqualTo(Body.Type.ANY_VALUE); + assertThat(logRecordData.getBody().asString()).isEqualTo("1"); + assertThat(((AnyValueBody) logRecordData.getBody()).asAnyValue()) + .isEqualTo(AnyValue.of(1)); + }); + exporter.reset(); + + // ...or a byte array of raw data + extendedLogRecordBuilder(logger) + .setBody(AnyValue.of("hello world".getBytes(StandardCharsets.UTF_8))) + .emit(); + assertThat(exporter.getFinishedLogRecordItems()) + .hasSize(1) + .satisfiesExactly( + logRecordData -> { + // TODO (jack-berg): add assertion when ANY_VALUE is added to Body.Type + // assertThat(logRecordData.getBody().getType()).isEqualTo(Body.Type.ANY_VALUE); + assertThat(logRecordData.getBody().asString()).isEqualTo("68656c6c6f20776f726c64"); + assertThat(((AnyValueBody) logRecordData.getBody()).asAnyValue()) + .isEqualTo(AnyValue.of("hello world".getBytes(StandardCharsets.UTF_8))); + }); + exporter.reset(); + + // But most commonly it will be used to represent complex structured like a map + extendedLogRecordBuilder(logger) + .setBody( + // The protocol data structure uses a repeated KeyValue to represent a map: + // https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/common/v1/common.proto#L59 + // The comment says that keys aren't allowed to repeat themselves, and because its + // represented as a repeated KeyValue, we need to at least offer the ability to preserve + // order. + // Accepting a Map> makes for a cleaner API, but ordering of the + // entries is lost. To accommodate use cases where ordering should be preserved we + // accept an array of key value pairs, but also a map based alternative (see the + // key_value_list_key entry). + AnyValue.of( + KeyAnyValue.of("str_key", AnyValue.of("value")), + KeyAnyValue.of("bool_key", AnyValue.of(true)), + KeyAnyValue.of("long_key", AnyValue.of(1L)), + KeyAnyValue.of("double_key", AnyValue.of(1.1)), + KeyAnyValue.of("bytes_key", AnyValue.of("bytes".getBytes(StandardCharsets.UTF_8))), + KeyAnyValue.of( + "arr_key", + AnyValue.of(AnyValue.of("entry1"), AnyValue.of(2), AnyValue.of(3.3))), + KeyAnyValue.of( + "key_value_list_key", + AnyValue.of( + new LinkedHashMap>() { + { + put("child_str_key1", AnyValue.of("child_value1")); + put("child_str_key2", AnyValue.of("child_value2")); + } + })))) + .emit(); + assertThat(exporter.getFinishedLogRecordItems()) + .hasSize(1) + .satisfiesExactly( + logRecordData -> { + // TODO (jack-berg): add assertion when ANY_VALUE is added to Body.Type + // assertThat(logRecordData.getBody().getType()).isEqualTo(Body.Type.ANY_VALUE); + assertThat(logRecordData.getBody().asString()) + .isEqualTo( + "[" + + "str_key=value, " + + "bool_key=true, " + + "long_key=1, " + + "double_key=1.1, " + + "bytes_key=6279746573, " + + "arr_key=[entry1, 2, 3.3], " + + "key_value_list_key=[child_str_key1=child_value1, child_str_key2=child_value2]" + + "]"); + assertThat(((AnyValueBody) logRecordData.getBody()).asAnyValue()) + .isEqualTo( + AnyValue.of( + KeyAnyValue.of("str_key", AnyValue.of("value")), + KeyAnyValue.of("bool_key", AnyValue.of(true)), + KeyAnyValue.of("long_key", AnyValue.of(1L)), + KeyAnyValue.of("double_key", AnyValue.of(1.1)), + KeyAnyValue.of( + "bytes_key", AnyValue.of("bytes".getBytes(StandardCharsets.UTF_8))), + KeyAnyValue.of( + "arr_key", + AnyValue.of(AnyValue.of("entry1"), AnyValue.of(2), AnyValue.of(3.3))), + KeyAnyValue.of( + "key_value_list_key", + AnyValue.of( + new LinkedHashMap>() { + { + put("child_str_key1", AnyValue.of("child_value1")); + put("child_str_key2", AnyValue.of("child_value2")); + } + })))); + }); + exporter.reset(); + + // ..or an array (optionally with heterogeneous types) + extendedLogRecordBuilder(logger) + .setBody(AnyValue.of(AnyValue.of("entry1"), AnyValue.of("entry2"), AnyValue.of(3))) + .emit(); + assertThat(exporter.getFinishedLogRecordItems()) + .hasSize(1) + .satisfiesExactly( + logRecordData -> { + // TODO (jack-berg): add assertion when ANY_VALUE is added to Body.Type + // assertThat(logRecordData.getBody().getType()).isEqualTo(Body.Type.ANY_VALUE); + assertThat(logRecordData.getBody().asString()).isEqualTo("[entry1, entry2, 3]"); + assertThat(((AnyValueBody) logRecordData.getBody()).asAnyValue()) + .isEqualTo( + AnyValue.of(AnyValue.of("entry1"), AnyValue.of("entry2"), AnyValue.of(3))); + }); + exporter.reset(); + } + + ExtendedLogRecordBuilder extendedLogRecordBuilder(Logger logger) { + return (ExtendedLogRecordBuilder) logger.logRecordBuilder(); + } +}