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:
+ *
+ *
+ * - Primitive values via {@link #of(long)}, {@link #of(String)}, {@link #of(boolean)}, {@link
+ * #of(double)}.
+ *
- String-keyed maps (i.e. associative arrays, dictionaries) via {@link #of(KeyAnyValue...)},
+ * {@link #of(Map)}. Note, because map values are type {@link AnyValue}, maps can be nested
+ * within other maps.
+ *
- Arrays (heterogeneous or homogenous) via {@link #of(AnyValue[])}. Note, because array
+ * values are type {@link AnyValue}, arrays can contain primitives, complex types like maps or
+ * arrays, or any combination.
+ *
- Raw bytes via {@link #of(byte[])}
+ *
+ *
+ * @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();
+ }
+}