aboutsummaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorBogdan Drutu <bdrutu@google.com>2018-08-02 15:57:31 +0300
committerGitHub <noreply@github.com>2018-08-02 15:57:31 +0300
commitd0d836136149d7379b50bd6fb8d81e4749df3fd8 (patch)
treeb420275a6ea4bb1eadad681b58524f2f9ee5ccc4 /api
parentdaa5d097e18211ad95a0e5bc63697e615db91902 (diff)
downloadopencensus-java-d0d836136149d7379b50bd6fb8d81e4749df3fd8.tar.gz
Add the initial version of the TraceState class. (#1300)
* Add the initial version of the TraceState class. * Add builder pattern. * Clean API and add tests. * Rename addOrUpdate to set and apply trailing rules for value. * Apply google java format. * Fix check framework.
Diffstat (limited to 'api')
-rw-r--r--api/src/main/java/io/opencensus/trace/Tracestate.java265
-rw-r--r--api/src/test/java/io/opencensus/trace/TracestateTest.java235
2 files changed, 500 insertions, 0 deletions
diff --git a/api/src/main/java/io/opencensus/trace/Tracestate.java b/api/src/main/java/io/opencensus/trace/Tracestate.java
new file mode 100644
index 00000000..63d32fd3
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracestate.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Carries tracing-system specific context in a list of key-value pairs. TraceState allows different
+ * vendors propagate additional information and inter-operate with their legacy Id formats.
+ *
+ * <p>Implementation is optimized for a small list of key-value pairs.
+ *
+ * <p>Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter,
+ * and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+ * forward slashes /.
+ *
+ * <p>Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the
+ * range 0x20 to 0x7E) except comma , and =.
+ *
+ * @since 0.16
+ */
+@Immutable
+@AutoValue
+@ExperimentalApi
+public abstract class Tracestate {
+ private static final int KEY_MAX_SIZE = 256;
+ private static final int VALUE_MAX_SIZE = 256;
+ private static final int MAX_KEY_VALUE_PAIRS = 32;
+
+ /**
+ * Returns the value to which the specified key is mapped, or null if this map contains no mapping
+ * for the key.
+ *
+ * @param key with which the specified value is to be associated
+ * @return the value to which the specified key is mapped, or null if this map contains no mapping
+ * for the key.
+ * @since 0.16
+ */
+ @javax.annotation.Nullable
+ public String get(String key) {
+ for (Entry entry : getEntries()) {
+ if (entry.getKey().equals(key)) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a {@link List} view of the mappings contained in this {@code TraceState}.
+ *
+ * @return a {@link List} view of the mappings contained in this {@code TraceState}.
+ * @since 0.16
+ */
+ public abstract List<Entry> getEntries();
+
+ /**
+ * Returns a builder based on this {@code Tracestate}.
+ *
+ * @return a builder based on this {@code Tracestate}.
+ * @since 0.16
+ */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Builder class for {@link MessageEvent}.
+ *
+ * @since 0.16
+ */
+ @ExperimentalApi
+ public static final class Builder {
+ private final Tracestate parent;
+ @javax.annotation.Nullable private ArrayList<Entry> entries;
+
+ private static final Tracestate EMPTY = create(Collections.<Entry>emptyList());
+
+ public Builder() {
+ this(EMPTY);
+ }
+
+ private Builder(Tracestate parent) {
+ Utils.checkNotNull(parent, "parent");
+ this.parent = parent;
+ this.entries = null;
+ }
+
+ /**
+ * Adds or updates the {@code Entry} that has the given {@code key} if it is present. The new
+ * {@code Entry} will always be added in the front of the list of entries.
+ *
+ * @param key the key for the {@code Entry} to be added.
+ * @param value the value for the {@code Entry} to be added.
+ * @return this.
+ * @since 0.16
+ */
+ @SuppressWarnings("nullness")
+ public Builder set(String key, String value) {
+ // Initially create the Entry to validate input.
+ Entry entry = Entry.create(key, value);
+ if (entries == null) {
+ // Copy entries from the parent.
+ entries = new ArrayList<Entry>(parent.getEntries());
+ }
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getKey().equals(entry.getKey())) {
+ entries.remove(i);
+ // Exit now because the entries list cannot contain duplicates.
+ break;
+ }
+ }
+ // Inserts the element at the front of this list.
+ entries.add(0, entry);
+ return this;
+ }
+
+ /**
+ * Removes the {@code Entry} that has the given {@code key} if it is present.
+ *
+ * @param key the key for the {@code Entry} to be removed.
+ * @return this.
+ * @since 0.16
+ */
+ @SuppressWarnings("nullness")
+ public Builder remove(String key) {
+ Utils.checkNotNull(key, "key");
+ if (entries == null) {
+ // Copy entries from the parent.
+ entries = new ArrayList<Entry>(parent.getEntries());
+ }
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getKey().equals(key)) {
+ entries.remove(i);
+ // Exit now because the entries list cannot contain duplicates.
+ break;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Builds a TraceState by adding the entries to the parent in front of the key-value pairs list
+ * and removing duplicate entries.
+ *
+ * @return a TraceState with the new entries.
+ * @since 0.16
+ */
+ public Tracestate build() {
+ if (entries == null) {
+ return parent;
+ }
+ return Tracestate.create(entries);
+ }
+ }
+
+ /**
+ * Immutable key-value pair for {@code Tracestate}.
+ *
+ * @since 0.16
+ */
+ @Immutable
+ @AutoValue
+ @ExperimentalApi
+ public abstract static class Entry {
+ /**
+ * Creates a new {@code Entry} for the {@code Tracestate}.
+ *
+ * @param key the Entry's key.
+ * @param value the Entry's value.
+ * @since 0.16
+ */
+ public static Entry create(String key, String value) {
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ Utils.checkArgument(validateKey(key), "Invalid key " + key);
+ Utils.checkArgument(validateValue(value), "Invalid value " + value);
+ return new AutoValue_Tracestate_Entry(key, value);
+ }
+
+ /**
+ * Returns the key {@code String}.
+ *
+ * @return the key {@code String}.
+ * @since 0.16
+ */
+ public abstract String getKey();
+
+ /**
+ * Returns the value {@code String}.
+ *
+ * @return the value {@code String}.
+ * @since 0.16
+ */
+ public abstract String getValue();
+
+ Entry() {}
+ }
+
+ // Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and
+ // can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+ // forward slashes /.
+ private static boolean validateKey(String key) {
+ if (key.length() > KEY_MAX_SIZE
+ || key.isEmpty()
+ || key.charAt(0) < 'a'
+ || key.charAt(0) > 'z') {
+ return false;
+ }
+ for (int i = 1; i < key.length(); i++) {
+ char c = key.charAt(i);
+ if (!(c >= 'a' && c <= 'z')
+ && !(c >= '0' && c <= '9')
+ && c != '_'
+ && c != '-'
+ && c != '*'
+ && c != '/') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range
+ // 0x20 to 0x7E) except comma , and =.
+ private static boolean validateValue(String value) {
+ if (value.length() > VALUE_MAX_SIZE || value.charAt(value.length() - 1) == ' ' /* '\u0020' */) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Tracestate create(List<Entry> entries) {
+ Utils.checkState(entries.size() <= MAX_KEY_VALUE_PAIRS, "Invalid size");
+ return new AutoValue_Tracestate(Collections.unmodifiableList(entries));
+ }
+
+ Tracestate() {}
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracestateTest.java b/api/src/test/java/io/opencensus/trace/TracestateTest.java
new file mode 100644
index 00000000..b347d8ad
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracestateTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.trace.Tracestate.Entry;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Tracestate}. */
+@RunWith(JUnit4.class)
+public class TracestateTest {
+ private static final String FIRST_KEY = "key_1";
+ private static final String SECOND_KEY = "key_2";
+ private static final String FIRST_VALUE = "value.1";
+ private static final String SECOND_VALUE = "value.2";
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final Tracestate EMPTY = new Tracestate.Builder().build();
+ private final Tracestate firstTracestate = EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build();
+ private final Tracestate secondTracestate =
+ EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build();
+ private final Tracestate multiValueTracestate =
+ EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+ @Test
+ public void get() {
+ assertThat(firstTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+ assertThat(secondTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ assertThat(multiValueTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+ assertThat(multiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ }
+
+ @Test
+ public void getEntries() {
+ assertThat(firstTracestate.getEntries()).containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+ assertThat(secondTracestate.getEntries())
+ .containsExactly(Entry.create(SECOND_KEY, SECOND_VALUE));
+ assertThat(multiValueTracestate.getEntries())
+ .containsExactly(
+ Entry.create(FIRST_KEY, FIRST_VALUE), Entry.create(SECOND_KEY, SECOND_VALUE));
+ }
+
+ @Test
+ public void disallowsNullKey() {
+ thrown.expect(NullPointerException.class);
+ EMPTY.toBuilder().set(null, FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidFirstKeyCharacter() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set("1_key", FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidKeyCharacters() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set("kEy_1", FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidKeySize() {
+ char[] chars = new char[257];
+ Arrays.fill(chars, 'a');
+ String longKey = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(longKey, FIRST_VALUE).build();
+ }
+
+ @Test
+ public void allAllowedKeyCharacters() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (char c = 'a'; c <= 'z'; c++) {
+ stringBuilder.append(c);
+ }
+ for (char c = '0'; c <= '9'; c++) {
+ stringBuilder.append(c);
+ }
+ stringBuilder.append('_');
+ stringBuilder.append('-');
+ stringBuilder.append('*');
+ stringBuilder.append('/');
+ String allowedKey = stringBuilder.toString();
+ assertThat(EMPTY.toBuilder().set(allowedKey, FIRST_VALUE).build().get(allowedKey))
+ .isEqualTo(FIRST_VALUE);
+ }
+
+ @Test
+ public void disallowsNullValue() {
+ thrown.expect(NullPointerException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, null).build();
+ }
+
+ @Test
+ public void valueCannotContainEqual() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "my_vakue=5").build();
+ }
+
+ @Test
+ public void valueCannotContainComma() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "first,second").build();
+ }
+
+ @Test
+ public void valueCannotContainTrailingSpaces() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "first ").build();
+ }
+
+ @Test
+ public void invalidValueSize() {
+ char[] chars = new char[257];
+ Arrays.fill(chars, 'a');
+ String longValue = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, longValue).build();
+ }
+
+ @Test
+ public void allAllowedValueCharacters() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (char c = ' ' /* '\u0020' */; c <= '~' /* '\u007E' */; c++) {
+ if (c == ',' || c == '=') {
+ continue;
+ }
+ stringBuilder.append(c);
+ }
+ String allowedValue = stringBuilder.toString();
+ assertThat(EMPTY.toBuilder().set(FIRST_KEY, allowedValue).build().get(FIRST_KEY))
+ .isEqualTo(allowedValue);
+ }
+
+ @Test
+ public void addEntry() {
+ assertThat(firstTracestate.toBuilder().set(SECOND_KEY, SECOND_VALUE).build())
+ .isEqualTo(multiValueTracestate);
+ }
+
+ @Test
+ public void updateEntry() {
+ assertThat(firstTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build().get(FIRST_KEY))
+ .isEqualTo(SECOND_VALUE);
+ Tracestate updatedMultiValueTracestate =
+ multiValueTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build();
+ assertThat(updatedMultiValueTracestate.get(FIRST_KEY)).isEqualTo(SECOND_VALUE);
+ assertThat(updatedMultiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ }
+
+ @Test
+ public void addAndUpdateEntry() {
+ assertThat(
+ firstTracestate
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .set(SECOND_KEY, FIRST_VALUE) // add a new entry
+ .build()
+ .getEntries())
+ .containsExactly(
+ Entry.create(FIRST_KEY, SECOND_VALUE), Entry.create(SECOND_KEY, FIRST_VALUE));
+ }
+
+ @Test
+ public void addSameKey() {
+ assertThat(
+ EMPTY
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .set(FIRST_KEY, FIRST_VALUE) // add a new entry
+ .build()
+ .getEntries())
+ .containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+ }
+
+ @Test
+ public void remove() {
+ assertThat(multiValueTracestate.toBuilder().remove(SECOND_KEY).build())
+ .isEqualTo(firstTracestate);
+ }
+
+ @Test
+ public void addAndRemoveEntry() {
+ assertThat(
+ EMPTY
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .remove(FIRST_KEY) // add a new entry
+ .build())
+ .isEqualTo(EMPTY);
+ }
+
+ @Test
+ public void remove_NullNotAllowed() {
+ thrown.expect(NullPointerException.class);
+ multiValueTracestate.toBuilder().remove(null).build();
+ }
+
+ @Test
+ public void tracestate_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(EMPTY, EMPTY);
+ tester.addEqualityGroup(firstTracestate, EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build());
+ tester.addEqualityGroup(
+ secondTracestate, EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build());
+ tester.testEquals();
+ }
+
+ @Test
+ public void tracestate_ToString() {
+ assertThat(EMPTY.toString()).isEqualTo("Tracestate{entries=[]}");
+ }
+}