opentelemetry-java/api/src/main/java/io/opentelemetry/trace/TraceState.java

308 lines
9.3 KiB
Java

/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.trace;
import com.google.auto.value.AutoValue;
import io.opentelemetry.internal.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
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.1.0
*/
@Immutable
@AutoValue
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;
private static final TraceState DEFAULT = TraceState.builder().build();
private static final int MAX_TENANT_ID_SIZE = 240;
public static final int MAX_VENDOR_ID_SIZE = 13;
/**
* Returns the default {@code TraceState} with no entries.
*
* @return the default {@code TraceState}.
* @since 0.1.0
*/
public static TraceState getDefault() {
return DEFAULT;
}
/**
* 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.1.0
*/
@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.1.0
*/
public abstract List<Entry> getEntries();
/**
* Returns a {@code Builder} based on an empty {@code TraceState}.
*
* @return a {@code Builder} based on an empty {@code TraceState}.
* @since 0.1.0
*/
public static Builder builder() {
return new Builder(Builder.EMPTY);
}
/**
* Returns a {@code Builder} based on this {@code TraceState}.
*
* @return a {@code Builder} based on this {@code TraceState}.
* @since 0.1.0
*/
public Builder toBuilder() {
return new Builder(this);
}
/**
* Builder class for {@link TraceState}.
*
* @since 0.1.0
*/
public static final class Builder {
private final TraceState parent;
@Nullable private ArrayList<Entry> entries;
// Needs to be in this class to avoid initialization deadlock because super class depends on
// subclass (the auto-value generate class).
private static final TraceState EMPTY = create(Collections.emptyList());
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.1.0
*/
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<>(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.1.0
*/
public Builder remove(String key) {
Utils.checkNotNull(key, "key");
if (entries == null) {
// Copy entries from the parent.
entries = new ArrayList<>(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.1.0
*/
public TraceState build() {
if (entries == null) {
return parent;
}
return TraceState.create(entries);
}
}
/**
* Immutable key-value pair for {@code TraceState}.
*
* @since 0.1.0
*/
@Immutable
@AutoValue
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.
* @return the new {@code Entry}.
* @since 0.1.0
*/
public static Entry create(String key, String value) {
Utils.checkNotNull(key, "key");
Utils.checkNotNull(value, "value");
Utils.checkArgument(validateKey(key), "Invalid key %s", key);
Utils.checkArgument(validateValue(value), "Invalid value %s", value);
return new AutoValue_TraceState_Entry(key, value);
}
/**
* Returns the key {@code String}.
*
* @return the key {@code String}.
* @since 0.1.0
*/
public abstract String getKey();
/**
* Returns the value {@code String}.
*
* @return the value {@code String}.
* @since 0.1.0
*/
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 /. For multi-tenant vendor scenarios, an at sign (@) can be used to prefix the
// vendor name. The tenant id (before the '@') is limited to 240 characters and the vendor id is
// limited to 13 characters. If in the multi-tenant vendor format, then the first character
// may additionally be digit.
//
// todo: benchmark this implementation
private static boolean validateKey(String key) {
if (key.length() > KEY_MAX_SIZE
|| key.isEmpty()
|| isNotLowercaseLetterOrDigit(key.charAt(0))) {
return false;
}
boolean isMultiTenantVendorKey = false;
for (int i = 1; i < key.length(); i++) {
char c = key.charAt(i);
if (isNotLegalKeyCharacter(c)) {
return false;
}
if (c == '@') {
// you can't have 2 '@' signs
if (isMultiTenantVendorKey) {
return false;
}
isMultiTenantVendorKey = true;
// tenant id (the part to the left of the '@' sign) must be 240 characters or less
if (i > MAX_TENANT_ID_SIZE) {
return false;
}
// vendor id (the part to the right of the '@' sign) must be 13 characters or less
if (key.length() - i > MAX_VENDOR_ID_SIZE) {
return false;
}
}
}
if (!isMultiTenantVendorKey) {
// if it's not the vendor format (with an '@' sign), the key must start with a letter.
return isNotDigit(key.charAt(0));
}
return true;
}
private static boolean isNotLegalKeyCharacter(char c) {
return isNotLowercaseLetterOrDigit(c)
&& c != '_'
&& c != '-'
&& c != '@'
&& c != '*'
&& c != '/';
}
private static boolean isNotLowercaseLetterOrDigit(char ch) {
return (ch < 'a' || ch > 'z') && isNotDigit(ch);
}
private static boolean isNotDigit(char ch) {
return ch < '0' || ch > '9';
}
// 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() {}
}