diff --git a/api/all/src/main/java/io/opentelemetry/api/common/Value.java b/api/all/src/main/java/io/opentelemetry/api/common/Value.java index a29be801e27..5de28596408 100644 --- a/api/all/src/main/java/io/opentelemetry/api/common/Value.java +++ b/api/all/src/main/java/io/opentelemetry/api/common/Value.java @@ -6,6 +6,8 @@ package io.opentelemetry.api.common; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,6 +86,77 @@ static Value> of(Map> value) { return KeyValueList.createFromMap(value); } + /** + * Convert an object of primitives, Lists, and Maps (nested in any manner) to {@link Value}. + * + *

Specifically, the following types are supported. If the {@code object} (or any nested data + * structures) contains an unsupported type, an {@link IllegalArgumentException} is thrown. + * + *

+ * + * @param object the object to convert + * @return the equivalent {@link Value} + * @throws IllegalArgumentException if not able to convert the object to {@link Value} + */ + @SuppressWarnings("ThrowsUncheckedException") + static Value convert(Object object) throws IllegalArgumentException { + if (object instanceof Integer) { + return Value.of((Integer) object); + } + if (object instanceof Long) { + return Value.of((Long) object); + } + if (object instanceof Float) { + return Value.of((Float) object); + } + if (object instanceof Double) { + return Value.of((Double) object); + } + if (object instanceof Boolean) { + return Value.of((Boolean) object); + } + if (object instanceof String) { + return Value.of((String) object); + } + if (object instanceof byte[]) { + return Value.of((byte[]) object); + } + if (object instanceof List) { + List list = (List) object; + List> valueList = new ArrayList<>(list.size()); + for (Object entry : list) { + valueList.add(Value.convert(entry)); + } + return Value.of(valueList); + } + if (object instanceof Map) { + Map map = (Map) object; + Map> valueMap = new HashMap<>(map.size()); + map.forEach( + (key, value) -> { + if (!(key instanceof String)) { + throw new IllegalArgumentException( + "Cannot convert map with key type " + + key.getClass().getSimpleName() + + " to value"); + } + valueMap.put((String) key, Value.convert(value)); + }); + return Value.of(valueMap); + } + throw new IllegalArgumentException( + "Cannot convert object of type " + object.getClass().getSimpleName() + " to value"); + } + /** Returns the type of this {@link Value}. Useful for building switch statements. */ ValueType getType(); diff --git a/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java index ae83e0dd44c..4f3d370df89 100644 --- a/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/logs/ValueTest.java @@ -9,9 +9,12 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.provider.Arguments.arguments; +import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.KeyValue; import io.opentelemetry.api.common.Value; import io.opentelemetry.api.common.ValueType; +import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ReadOnlyBufferException; import java.nio.charset.StandardCharsets; @@ -20,7 +23,9 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; +import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -212,4 +217,74 @@ void valueByteAsString() { byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded); assertThat(new String(decodedBytes, StandardCharsets.UTF_8)).isEqualTo(str); } + + @ParameterizedTest + @MethodSource("convertArgs") + void convert(Object object, Value expected) { + Value converted = Value.convert(object); + if (Objects.requireNonNull(expected.getType()) == ValueType.DOUBLE) { + assertThat(converted.getType()).isEqualTo(ValueType.DOUBLE); + assertThat((Double) converted.getValue()) + .isEqualTo((Double) expected.getValue(), Offset.offset(.001)); + } else { + assertThat(converted).isEqualTo(expected); + } + } + + @SuppressWarnings("cast") + private static Stream convertArgs() { + return Stream.of( + Arguments.of((int) 1, Value.of(1)), + Arguments.of(1L, Value.of(1L)), + Arguments.of(Long.valueOf(1L), Value.of(1L)), + Arguments.of((float) 1.1, Value.of(1.1)), + Arguments.of(1.1D, Value.of(1.1)), + Arguments.of(Double.valueOf(1.1D), Value.of(1.1)), + Arguments.of(true, Value.of(true)), + Arguments.of(Boolean.valueOf(true), Value.of(true)), + Arguments.of("value", Value.of("value")), + Arguments.of( + "value".getBytes(StandardCharsets.UTF_8), + Value.of("value".getBytes(StandardCharsets.UTF_8))), + Arguments.of( + Arrays.asList("value1", "value2"), + Value.of(Arrays.asList(Value.of("value1"), Value.of("value2")))), + Arguments.of( + Arrays.asList("value", true), + Value.of(Arrays.asList(Value.of("value"), Value.of(true)))), + Arguments.of( + ImmutableMap.builder().put("key1", "value").put("key2", true).build(), + Value.of( + ImmutableMap.>builder() + .put("key1", Value.of("value")) + .put("key2", Value.of(true)) + .build())), + Arguments.of( + ImmutableMap.builder() + .put("key1", "value") + .put("key2", true) + .put("key3", Collections.singletonMap("key4", "value")) + .build(), + Value.of( + ImmutableMap.>builder() + .put("key1", Value.of("value")) + .put("key2", Value.of(true)) + .put("key3", Value.of(Collections.singletonMap("key4", Value.of("value")))) + .build()))); + } + + @ParameterizedTest + @MethodSource("convertUnsupportedArgs") + void convertUnsupported(Object object) { + assertThatThrownBy(() -> Value.convert(object)).isInstanceOf(IllegalArgumentException.class); + } + + private static Stream convertUnsupportedArgs() { + return Stream.of( + Arguments.of(new Object()), + Arguments.of(new BigInteger("1")), + Arguments.of(new BigDecimal("1.1")), + Arguments.of(Collections.singletonList(new Object())), + Arguments.of(Collections.singletonMap(new Object(), "value"))); + } } diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index e1abf63b48c..313b36ab56d 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,4 +1,9 @@ Comparing source compatibility of opentelemetry-api-1.57.0-SNAPSHOT.jar against opentelemetry-api-1.56.0.jar +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.common.Value (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + GENERIC TEMPLATES: === T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.common.Value convert(java.lang.Object) + +++ NEW EXCEPTION: java.lang.IllegalArgumentException *** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.api.GlobalOpenTelemetry (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.OpenTelemetry getOrNoop()