Immutable Attributes and Labels (#1304)

* prototype for class to replace Map<String, AttributeValue)

* safely copy the builder's data

* add the empty constant

* optimization for attributes with a single key/value

* Add an iterator/iterable and some simple tests

* fix animalsniffer complaint

* tests for de-duping and order-independent equality, plus removal of possibly unneeded access methods.

* clean up the sort&filter method a tad

* replace the iterator with a foreach method

* Make the Attributes parameterized by the value type.

* Add basic javadoc

* remove helper class; add a simple test for the builder; make the tests more robust

* Add a varargs method for creating an arbitrary number of key/value pairs.

* static import the check method, for consistency

* Refactor to have an interface and two implementations, with some shared logic.

* fix an accidental rename

* really fix it for real

* add a few more tests

* preserve the `setAttribute` names from existing Span API

* Replace the treemap sorting and filtering with a quicksort and post-filter.

* remove an unneeded list.

* switch to an abstract base class to remove some code duplication

* Updated docs based on feedback.

* Small change to use the builder for the empty implementations.
This commit is contained in:
John Watson 2020-06-08 17:16:39 -07:00 committed by GitHub
parent afabbd1e06
commit ee435201b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 806 additions and 0 deletions

View File

@ -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.
*
* <p>The keys are {@link String}s and the values are {@link AttributeValue} instances.
*/
@Immutable
public abstract class Attributes extends ImmutableKeyValuePairs<AttributeValue> {
private static final Attributes EMPTY = Attributes.newBuilder().build();
@AutoValue
@Immutable
abstract static class ArrayBackedAttributes extends Attributes {
ArrayBackedAttributes() {}
@Override
abstract List<Object> 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<Object> 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;
}
}
}

View File

@ -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 <V> The type of the values contained in this.
* @see Labels
* @see Attributes
*/
@Immutable
abstract class ImmutableKeyValuePairs<V> {
private static final Logger logger = Logger.getLogger(ImmutableKeyValuePairs.class.getName());
List<Object> data() {
return Collections.emptyList();
}
/** Iterates over all the key-value pairs of attributes contained by this instance. */
@SuppressWarnings("unchecked")
public void forEach(KeyValueConsumer<V> consumer) {
for (int i = 0; i < data().size(); i += 2) {
consumer.consume((String) data().get(i), (V) data().get(i + 1));
}
}
static List<Object> 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<Object> dedupe(Object[] data) {
List<Object> 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;
}
}

View File

@ -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<T> {
void consume(String key, T value);
}

View File

@ -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<String> {
private static final Labels EMPTY = Labels.newBuilder().build();
@AutoValue
@Immutable
abstract static class ArrayBackedLabels extends Labels {
ArrayBackedLabels() {}
@Override
abstract List<Object> 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<Object> 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;
}
}
}

View File

@ -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<String, AttributeValue> entriesSeen = new HashMap<>();
Attributes attributes =
Attributes.of(
"key1", stringAttributeValue("value1"),
"key2", AttributeValue.longAttributeValue(333));
attributes.forEach(
new KeyValueConsumer<AttributeValue>() {
@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<String, AttributeValue> entriesSeen = new HashMap<>();
Attributes attributes = Attributes.of("key", stringAttributeValue("value"));
attributes.forEach(
new KeyValueConsumer<AttributeValue>() {
@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<AttributeValue>() {
@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)));
}
}

View File

@ -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<String, String> entriesSeen = new HashMap<>();
Labels labels =
Labels.of(
"key1", "value1",
"key2", "value2");
labels.forEach(
new KeyValueConsumer<String>() {
@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<String, String> entriesSeen = new HashMap<>();
Labels labels = Labels.of("key", "value");
labels.forEach(
new KeyValueConsumer<String>() {
@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<String>() {
@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"));
}
}