Experimental support for Log AnyValue body (#5880)

This commit is contained in:
jack-berg 2023-10-31 15:05:36 -05:00 committed by GitHub
parent ccb2e04237
commit efa46a5dcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1041 additions and 2 deletions

View File

@ -41,7 +41,7 @@ final class LogMarshaler extends MarshalerWithSize {
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()));

View File

@ -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"))
}

View File

@ -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 <a
* href="https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/common/v1/common.proto#L28">AnyValue</a>
* message type, and is used to model any type.
*
* <p>It can be used to represent:
*
* <ul>
* <li>Primitive values via {@link #of(long)}, {@link #of(String)}, {@link #of(boolean)}, {@link
* #of(double)}.
* <li>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.
* <li>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.
* <li>Raw bytes via {@link #of(byte[])}
* </ul>
*
* @param <T> the type. See {@link #getValue()} for description of types.
*/
public interface AnyValue<T> {
/** Returns an {@link AnyValue} for the {@link String} value. */
static AnyValue<String> of(String value) {
return AnyValueString.create(value);
}
/** Returns an {@link AnyValue} for the {@code boolean} value. */
static AnyValue<Boolean> of(boolean value) {
return AnyValueBoolean.create(value);
}
/** Returns an {@link AnyValue} for the {@code long} value. */
static AnyValue<Long> of(long value) {
return AnyValueLong.create(value);
}
/** Returns an {@link AnyValue} for the {@code double} value. */
static AnyValue<Double> of(double value) {
return AnyValueDouble.create(value);
}
/** Returns an {@link AnyValue} for the {@code byte[]} value. */
static AnyValue<ByteBuffer> of(byte[] value) {
return AnyValueBytes.create(value);
}
/** Returns an {@link AnyValue} for the array of {@link AnyValue} values. */
static AnyValue<List<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<List<KeyAnyValue>> of(KeyAnyValue... value) {
return KeyAnyValueList.create(value);
}
/** Returns an {@link AnyValue} for the {@link Map} of key, {@link AnyValue}. */
static AnyValue<List<KeyAnyValue>> of(Map<String, AnyValue<?>> 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}.
*
* <p>The return type varies by {@link #getType()} as described below:
*
* <ul>
* <li>{@link AnyValueType#STRING} returns {@link String}
* <li>{@link AnyValueType#BOOLEAN} returns {@code boolean}
* <li>{@link AnyValueType#LONG} returns {@code long}
* <li>{@link AnyValueType#DOUBLE} returns {@code double}
* <li>{@link AnyValueType#ARRAY} returns {@link List} of {@link AnyValue}
* <li>{@link AnyValueType#KEY_VALUE_LIST} returns {@link List} of {@link KeyAnyValue}
* <li>{@link AnyValueType#BYTES} returns read only {@link ByteBuffer}. See {@link
* ByteBuffer#asReadOnlyBuffer()}.
* </ul>
*/
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();
}

View File

@ -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<List<AnyValue<?>>> {
private final List<AnyValue<?>> value;
private AnyValueArray(List<AnyValue<?>> value) {
this.value = value;
}
static AnyValue<List<AnyValue<?>>> create(AnyValue<?>... value) {
Objects.requireNonNull(value, "value must not be null");
List<AnyValue<?>> 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<AnyValue<?>> 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();
}
}

View File

@ -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<Boolean> {
private final boolean value;
private AnyValueBoolean(boolean value) {
this.value = value;
}
static AnyValue<Boolean> 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);
}
}

View File

@ -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<ByteBuffer> {
private final byte[] raw;
private AnyValueBytes(byte[] value) {
this.raw = value;
}
static AnyValue<ByteBuffer> 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);
}
}

View File

@ -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<Double> {
private final double value;
private AnyValueDouble(double value) {
this.value = value;
}
static AnyValue<Double> 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);
}
}

View File

@ -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<Long> {
private final long value;
private AnyValueLong(long value) {
this.value = value;
}
static AnyValue<Long> 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);
}
}

View File

@ -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<String> {
private final String value;
private AnyValueString(String value) {
this.value = value;
}
static AnyValue<String> 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();
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.extension.incubator.logs;
/**
* AnyValue type options, mirroring <a
* href="https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/common/v1/common.proto#L31">AnyValue#value
* options</a>.
*/
public enum AnyValueType {
STRING,
BOOLEAN,
LONG,
DOUBLE,
ARRAY,
KEY_VALUE_LIST,
BYTES
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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<List<KeyAnyValue>> {
private final List<KeyAnyValue> value;
private KeyAnyValueList(List<KeyAnyValue> value) {
this.value = value;
}
static AnyValue<List<KeyAnyValue>> create(KeyAnyValue... value) {
Objects.requireNonNull(value, "value must not be null");
List<KeyAnyValue> list = new ArrayList<>(value.length);
list.addAll(Arrays.asList(value));
return new KeyAnyValueList(Collections.unmodifiableList(list));
}
static AnyValue<List<KeyAnyValue>> createFromMap(Map<String, AnyValue<?>> 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<KeyAnyValue> 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();
}
}

View File

@ -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<String, AnyValue<?>>() {
{
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<String, AnyValue<?>>() {
{
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<String, AnyValue<?>>) 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<Arguments> 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<String, AnyValue<?>>() {
{
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);
}
}

View File

@ -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")
}

View File

@ -10,15 +10,18 @@ import io.opentelemetry.api.logs.LogRecordBuilder;
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 @@ final class SdkLogRecordBuilder implements LogRecordBuilder {
return this;
}
@Override
public LogRecordBuilder setBody(AnyValue<?> value) {
this.body = AnyValueBody.create(value);
return this;
}
@Override
public <T> SdkLogRecordBuilder setAttribute(AttributeKey<T> key, T value) {
if (key == null || key.getKey().isEmpty() || value == null) {

View File

@ -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
}
/**

View File

@ -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() + "}";
}
}

View File

@ -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<String, AnyValue<?>> 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<String, AnyValue<?>>() {
{
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<String, AnyValue<?>>() {
{
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();
}
}