From ee435201b51f15ca11ab24390e736834332b450c Mon Sep 17 00:00:00 2001 From: John Watson Date: Mon, 8 Jun 2020 17:16:39 -0700 Subject: [PATCH] Immutable Attributes and Labels (#1304) * prototype for class to replace MapThe keys are {@link String}s and the values are {@link AttributeValue} instances. + */ +@Immutable +public abstract class Attributes extends ImmutableKeyValuePairs { + private static final Attributes EMPTY = Attributes.newBuilder().build(); + + @AutoValue + @Immutable + abstract static class ArrayBackedAttributes extends Attributes { + ArrayBackedAttributes() {} + + @Override + abstract List data(); + } + + /** Returns a {@link Attributes} instance with no attributes. */ + public static Attributes empty() { + return EMPTY; + } + + /** Returns a {@link Attributes} instance with a single key-value pair. */ + public static Attributes of(String key, AttributeValue value) { + return sortAndFilterToAttributes(key, value); + } + + /** + * Returns a {@link Attributes} instance with two key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Attributes of( + String key1, AttributeValue value1, String key2, AttributeValue value2) { + return sortAndFilterToAttributes(key1, value1, key2, value2); + } + + /** + * Returns a {@link Attributes} instance with three key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Attributes of( + String key1, + AttributeValue value1, + String key2, + AttributeValue value2, + String key3, + AttributeValue value3) { + return sortAndFilterToAttributes(key1, value1, key2, value2, key3, value3); + } + + /** + * Returns a {@link Attributes} instance with four key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Attributes of( + String key1, + AttributeValue value1, + String key2, + AttributeValue value2, + String key3, + AttributeValue value3, + String key4, + AttributeValue value4) { + return sortAndFilterToAttributes(key1, value1, key2, value2, key3, value3, key4, value4); + } + + /** + * Returns a {@link Attributes} instance with five key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Attributes of( + String key1, + AttributeValue value1, + String key2, + AttributeValue value2, + String key3, + AttributeValue value3, + String key4, + AttributeValue value4, + String key5, + AttributeValue value5) { + return sortAndFilterToAttributes( + key1, value1, + key2, value2, + key3, value3, + key4, value4, + key5, value5); + } + + private static Attributes sortAndFilterToAttributes(Object... data) { + return new AutoValue_Attributes_ArrayBackedAttributes(sortAndFilter(data)); + } + + /** Creates a new {@link Builder} instance for creating arbitrary {@link Attributes}. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Enables the creation of an {@link Attributes} instance with an arbitrary number of key-value + * pairs. + */ + public static class Builder { + private final List data = new ArrayList<>(); + + /** Create the {@link Attributes} from this. */ + public Attributes build() { + return sortAndFilterToAttributes(data.toArray()); + } + + /** + * Sets a bare {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, AttributeValue value) { + data.add(key); + data.add(value); + return this; + } + + /** + * Sets a String {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, String value) { + data.add(key); + data.add(stringAttributeValue(value)); + return this; + } + + /** + * Sets a long {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, long value) { + data.add(key); + data.add(longAttributeValue(value)); + return this; + } + + /** + * Sets a double {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, double value) { + data.add(key); + data.add(doubleAttributeValue(value)); + return this; + } + + /** + * Sets a boolean {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, boolean value) { + data.add(key); + data.add(booleanAttributeValue(value)); + return this; + } + + /** + * Sets a String array {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, String... value) { + data.add(key); + data.add(arrayAttributeValue(value)); + return this; + } + + /** + * Sets a Long array {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, Long... value) { + data.add(key); + data.add(arrayAttributeValue(value)); + return this; + } + + /** + * Sets a Double array {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, Double... value) { + data.add(key); + data.add(arrayAttributeValue(value)); + return this; + } + + /** + * Sets a Boolean array {@link AttributeValue} into this. + * + * @return this Builder + */ + public Builder setAttribute(String key, Boolean... value) { + data.add(key); + data.add(arrayAttributeValue(value)); + return this; + } + } +} diff --git a/api/src/main/java/io/opentelemetry/common/ImmutableKeyValuePairs.java b/api/src/main/java/io/opentelemetry/common/ImmutableKeyValuePairs.java new file mode 100644 index 0000000000..22625cb39e --- /dev/null +++ b/api/src/main/java/io/opentelemetry/common/ImmutableKeyValuePairs.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.common; + +import static io.opentelemetry.internal.Utils.checkArgument; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import javax.annotation.concurrent.Immutable; + +/** + * An immutable set of key-value pairs. Keys are only {@link String} typed. Can be iterated over + * using the {@link #forEach(KeyValueConsumer)} method. + * + * @param The type of the values contained in this. + * @see Labels + * @see Attributes + */ +@Immutable +abstract class ImmutableKeyValuePairs { + private static final Logger logger = Logger.getLogger(ImmutableKeyValuePairs.class.getName()); + + List data() { + return Collections.emptyList(); + } + + /** Iterates over all the key-value pairs of attributes contained by this instance. */ + @SuppressWarnings("unchecked") + public void forEach(KeyValueConsumer consumer) { + for (int i = 0; i < data().size(); i += 2) { + consumer.consume((String) data().get(i), (V) data().get(i + 1)); + } + } + + static List sortAndFilter(Object[] data) { + checkArgument( + data.length % 2 == 0, "You must provide an even number of key/value pair arguments."); + + quickSort(data, 0, data.length - 2); + return dedupe(data); + } + + private static void quickSort(Object[] data, int leftIndex, int rightIndex) { + if (leftIndex >= rightIndex) { + return; + } + + String pivotKey = (String) data[rightIndex]; + int counter = leftIndex; + + for (int i = leftIndex; i <= rightIndex; i += 2) { + if (((String) data[i]).compareTo(pivotKey) <= 0) { + swap(data, counter, i); + counter += 2; + } + } + + quickSort(data, leftIndex, counter - 4); + quickSort(data, counter, rightIndex); + } + + private static List dedupe(Object[] data) { + List result = new ArrayList<>(data.length); + Object previousKey = null; + + for (int i = 0; i < data.length; i += 2) { + Object key = data[i]; + Object value = data[i + 1]; + if (key == null) { + logger.warning("Ignoring null key."); + continue; + } + if (key.equals(previousKey)) { + continue; + } + previousKey = key; + result.add(key); + result.add(value); + } + return result; + } + + private static void swap(Object[] data, int a, int b) { + Object keyA = data[a]; + Object valueA = data[a + 1]; + data[a] = data[b]; + data[a + 1] = data[b + 1]; + + data[b] = keyA; + data[b + 1] = valueA; + } +} diff --git a/api/src/main/java/io/opentelemetry/common/KeyValueConsumer.java b/api/src/main/java/io/opentelemetry/common/KeyValueConsumer.java new file mode 100644 index 0000000000..c729795c87 --- /dev/null +++ b/api/src/main/java/io/opentelemetry/common/KeyValueConsumer.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.common; + +/** + * Used for iterating over the key-value pairs in a key-value pair container, such as {@link + * Attributes} or {@link Labels}. The key is always a {@link String}. + */ +public interface KeyValueConsumer { + void consume(String key, T value); +} diff --git a/api/src/main/java/io/opentelemetry/common/Labels.java b/api/src/main/java/io/opentelemetry/common/Labels.java new file mode 100644 index 0000000000..3eacf7cf0f --- /dev/null +++ b/api/src/main/java/io/opentelemetry/common/Labels.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.common; + +import com.google.auto.value.AutoValue; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** An immutable container for labels, which are pairs of {@link String}. */ +@Immutable +public abstract class Labels extends ImmutableKeyValuePairs { + + private static final Labels EMPTY = Labels.newBuilder().build(); + + @AutoValue + @Immutable + abstract static class ArrayBackedLabels extends Labels { + ArrayBackedLabels() {} + + @Override + abstract List data(); + } + + /** Returns a {@link Labels} instance with no attributes. */ + public static Labels empty() { + return EMPTY; + } + + /** Returns a {@link Labels} instance with a single key-value pair. */ + public static Labels of(String key, String value) { + return sortAndFilterToLabels(key, value); + } + + /** + * Returns a {@link Labels} instance with two key-value pairs. Order of the keys is not preserved. + * Duplicate keys will be removed. + */ + public static Labels of(String key1, String value1, String key2, String value2) { + return sortAndFilterToLabels(key1, value1, key2, value2); + } + + /** + * Returns a {@link Labels} instance with three key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Labels of( + String key1, String value1, String key2, String value2, String key3, String value3) { + return sortAndFilterToLabels(key1, value1, key2, value2, key3, value3); + } + + /** + * Returns a {@link Labels} instance with four key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Labels of( + String key1, + String value1, + String key2, + String value2, + String key3, + String value3, + String key4, + String value4) { + return sortAndFilterToLabels(key1, value1, key2, value2, key3, value3, key4, value4); + } + + /** + * Returns a {@link Labels} instance with five key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + public static Labels of( + String key1, + String value1, + String key2, + String value2, + String key3, + String value3, + String key4, + String value4, + String key5, + String value5) { + return sortAndFilterToLabels( + key1, value1, + key2, value2, + key3, value3, + key4, value4, + key5, value5); + } + + private static Labels sortAndFilterToLabels(Object... data) { + return new AutoValue_Labels_ArrayBackedLabels(sortAndFilter(data)); + } + + /** Creates a new {@link Builder} instance for creating arbitrary {@link Labels}. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Enables the creation of an {@link Labels} instance with an arbitrary number of key-value pairs. + */ + public static class Builder { + private final List data = new ArrayList<>(); + + /** Create the {@link Labels} from this. */ + public Labels build() { + return sortAndFilterToLabels(data.toArray()); + } + + /** + * Sets a single label into this Builder. + * + * @return this Builder + */ + public Builder setLabel(String key, String value) { + data.add(key); + data.add(value); + return this; + } + } +} diff --git a/api/src/test/java/io/opentelemetry/common/AttributesTest.java b/api/src/test/java/io/opentelemetry/common/AttributesTest.java new file mode 100644 index 0000000000..c735ad42ae --- /dev/null +++ b/api/src/test/java/io/opentelemetry/common/AttributesTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.common; + +import static com.google.common.truth.Truth.assertThat; +import static io.opentelemetry.common.AttributeValue.arrayAttributeValue; +import static io.opentelemetry.common.AttributeValue.booleanAttributeValue; +import static io.opentelemetry.common.AttributeValue.doubleAttributeValue; +import static io.opentelemetry.common.AttributeValue.longAttributeValue; +import static io.opentelemetry.common.AttributeValue.stringAttributeValue; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** Unit tests for {@link Attributes}s. */ +public class AttributesTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void forEach() { + final Map entriesSeen = new HashMap<>(); + + Attributes attributes = + Attributes.of( + "key1", stringAttributeValue("value1"), + "key2", AttributeValue.longAttributeValue(333)); + + attributes.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, AttributeValue value) { + entriesSeen.put(key, value); + } + }); + + assertThat(entriesSeen) + .containsExactly("key1", stringAttributeValue("value1"), "key2", longAttributeValue(333)); + } + + @Test + public void forEach_singleAttribute() { + final Map entriesSeen = new HashMap<>(); + + Attributes attributes = Attributes.of("key", stringAttributeValue("value")); + attributes.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, AttributeValue value) { + entriesSeen.put(key, value); + } + }); + assertThat(entriesSeen).containsExactly("key", stringAttributeValue("value")); + } + + @Test + public void forEach_empty() { + final AtomicBoolean sawSomething = new AtomicBoolean(false); + Attributes emptyAttributes = Attributes.empty(); + emptyAttributes.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, AttributeValue value) { + sawSomething.set(true); + } + }); + assertThat(sawSomething.get()).isFalse(); + } + + @Test + public void orderIndependentEquality() { + Attributes one = + Attributes.of( + "key1", stringAttributeValue("value1"), + "key2", stringAttributeValue("value2")); + Attributes two = + Attributes.of( + "key2", stringAttributeValue("value2"), + "key1", stringAttributeValue("value1")); + + assertThat(one).isEqualTo(two); + } + + @Test + public void deduplication() { + Attributes one = + Attributes.of( + "key1", stringAttributeValue("value1"), + "key1", stringAttributeValue("valueX")); + Attributes two = Attributes.of("key1", stringAttributeValue("value1")); + + assertThat(one).isEqualTo(two); + } + + @Test + public void builder() { + Attributes attributes = + Attributes.newBuilder() + .setAttribute("string", "value1") + .setAttribute("long", 100) + .setAttribute("double", 33.44) + .setAttribute("boolean", false) + .setAttribute("boolean", "duplicateShouldBeRemoved") + .build(); + + assertThat(attributes) + .isEqualTo( + Attributes.of( + "string", stringAttributeValue("value1"), + "long", longAttributeValue(100), + "double", doubleAttributeValue(33.44), + "boolean", booleanAttributeValue(false))); + } + + @Test + public void builder_arrayTypes() { + Attributes attributes = + Attributes.newBuilder() + .setAttribute("string", "value1", "value2") + .setAttribute("long", 100L, 200L) + .setAttribute("double", 33.44, -44.33) + .setAttribute("boolean", false, true) + .setAttribute("boolean", "duplicateShouldBeRemoved") + .setAttribute("boolean", stringAttributeValue("dropped")) + .build(); + + assertThat(attributes) + .isEqualTo( + Attributes.of( + "string", arrayAttributeValue("value1", "value2"), + "long", arrayAttributeValue(100L, 200L), + "double", arrayAttributeValue(33.44, -44.33), + "boolean", arrayAttributeValue(false, true))); + } +} diff --git a/api/src/test/java/io/opentelemetry/common/LabelsTest.java b/api/src/test/java/io/opentelemetry/common/LabelsTest.java new file mode 100644 index 0000000000..3e9e6c4405 --- /dev/null +++ b/api/src/test/java/io/opentelemetry/common/LabelsTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.common; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** Unit tests for {@link Labels}s. */ +public class LabelsTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void forEach() { + final Map entriesSeen = new HashMap<>(); + + Labels labels = + Labels.of( + "key1", "value1", + "key2", "value2"); + + labels.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, String value) { + entriesSeen.put(key, value); + } + }); + + assertThat(entriesSeen).containsExactly("key1", "value1", "key2", "value2"); + } + + @Test + public void forEach_singleAttribute() { + final Map entriesSeen = new HashMap<>(); + + Labels labels = Labels.of("key", "value"); + labels.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, String value) { + entriesSeen.put(key, value); + } + }); + + assertThat(entriesSeen).containsExactly("key", "value"); + } + + @Test + public void forEach_empty() { + final AtomicBoolean sawSomething = new AtomicBoolean(false); + Labels emptyLabels = Labels.empty(); + emptyLabels.forEach( + new KeyValueConsumer() { + @Override + public void consume(String key, String value) { + sawSomething.set(true); + } + }); + assertThat(sawSomething.get()).isFalse(); + } + + @Test + public void orderIndependentEquality() { + Labels one = + Labels.of( + "key3", "value3", + "key1", "value1", + "key2", "value2"); + Labels two = + Labels.of( + "key2", "value2", + "key3", "value3", + "key1", "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + public void deduplication() { + Labels one = + Labels.of( + "key1", "value1", + "key1", "valueX"); + Labels two = Labels.of("key1", "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + public void threeLabels() { + Labels one = + Labels.of( + "key1", "value1", + "key3", "value3", + "key2", "value2"); + assertThat(one).isNotNull(); + } + + @Test + public void fourLabels() { + Labels one = + Labels.of( + "key1", "value1", + "key2", "value2", + "key3", "value3", + "key4", "value4"); + assertThat(one).isNotNull(); + } + + @Test + public void builder() { + Labels labels = + Labels.newBuilder() + .setLabel("key1", "value1") + .setLabel("key2", "value2") + .setLabel("key1", "duplicateShouldBeIgnored") + .build(); + + assertThat(labels) + .isEqualTo( + Labels.of( + "key1", "value1", + "key2", "value2")); + } +}