diff --git a/api/src/main/java/io/opentelemetry/common/Attributes.java b/api/src/main/java/io/opentelemetry/common/Attributes.java
new file mode 100644
index 0000000000..efe07cd8da
--- /dev/null
+++ b/api/src/main/java/io/opentelemetry/common/Attributes.java
@@ -0,0 +1,240 @@
+/*
+ * 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.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 com.google.auto.value.AutoValue;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An immutable container for attributes.
+ *
+ *
The 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"));
+ }
+}