Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ public Attributes ofFive() {
values.get(4));
}

@Benchmark
@BenchmarkMode({Mode.AverageTime})
@Fork(1)
@Measurement(iterations = 15, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1)
public Attributes builderOneItem() {
return Attributes.builder().put(keys.get(0), values.get(0)).build();
}

@Benchmark
@BenchmarkMode({Mode.AverageTime})
@Fork(1)
Expand All @@ -134,4 +144,14 @@ public Attributes builderTenItems() {
}
return attributesBuilder.build();
}

@Benchmark
@BenchmarkMode({Mode.AverageTime})
@Fork(1)
@Measurement(iterations = 15, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1)
public Attributes putAllTenItems() {
return Attributes.builder().putAll(attributes.get(9)).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static io.opentelemetry.api.common.AttributeKey.stringArrayKey;
import static io.opentelemetry.api.common.AttributeKey.stringKey;

import io.opentelemetry.api.internal.AttributeValueLimits;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -23,22 +24,78 @@
class ArrayBackedAttributesBuilder implements AttributesBuilder {
private final List<Object> data;

/** Max number of unique entries. {@link Integer#MAX_VALUE} means unlimited. */
private final int countLimit;

/** Max length of string / byte-array values. {@link Integer#MAX_VALUE} means unlimited. */
private final int valueLengthLimit;

/** Max nesting depth for array / map values. {@link Integer#MAX_VALUE} means unlimited. */
private final int valueDepthLimit;

/** Count of non-null entries currently stored (excludes null holes from remove). */
private int size;

/**
* Cached {@link #build()} result; null when unset or after a mutation. Only maintained when
* limits are configured.
*/
@Nullable private Attributes cachedBuild;

ArrayBackedAttributesBuilder() {
data = new ArrayList<>();
this(new ArrayList<>(), Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
}

ArrayBackedAttributesBuilder(List<Object> data) {
this(data, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, data.size() / 2);
}

ArrayBackedAttributesBuilder(AttributeLimits limits) {
this(
new ArrayList<>(),
limits.getCountLimit(),
limits.getValueLengthLimit(),
limits.getValueDepthLimit(),
0);
}

private ArrayBackedAttributesBuilder(
List<Object> data,
int countLimit,
int valueLengthLimit,
int valueDepthLimit,
int initialSize) {
this.data = data;
this.countLimit = countLimit;
this.valueLengthLimit = valueLengthLimit;
this.valueDepthLimit = valueDepthLimit;
this.size = initialSize;
}

private boolean isLimited() {
return countLimit != Integer.MAX_VALUE
|| valueLengthLimit != Integer.MAX_VALUE
|| valueDepthLimit != Integer.MAX_VALUE;
}

@Override
public Attributes build() {
Attributes cached = cachedBuild;
if (cached != null) {
return cached;
}
Attributes result;
// If only one key-value pair AND the entry hasn't been set to null (by #remove(AttributeKey<T>)
// or #removeIf(Predicate<AttributeKey<?>>)), then we can bypass sorting and filtering
if (data.size() == 2 && data.get(0) != null) {
return new ArrayBackedAttributes(data.toArray());
result = new ArrayBackedAttributes(data.toArray());
} else {
result = ArrayBackedAttributes.sortAndFilterToAttributes(data.toArray());
}
if (isLimited()) {
cachedBuild = result;
}
return ArrayBackedAttributes.sortAndFilterToAttributes(data.toArray());
return result;
}

@Override
Expand All @@ -55,11 +112,49 @@ public <T> AttributesBuilder put(AttributeKey<T> key, @Nullable T value) {
putValue(key, (Value<?>) value);
return this;
}
data.add(key);
data.add(value);
addPair(key, value);
return this;
}

/** Append (unlimited) or dedup-by-name / truncate / capacity-check (limited). */
private void addPair(AttributeKey<?> key, Object value) {
if (!isLimited()) {
data.add(key);
data.add(value);
size++;
return;
}
cachedBuild = null;
Object limited = AttributeValueLimits.apply(value, valueLengthLimit, valueDepthLimit);
String name = key.getKey();
int emptySlot = -1;
for (int i = 0; i < data.size(); i += 2) {
Object existing = data.get(i);
if (existing == null) {
if (emptySlot < 0) {
emptySlot = i;
}
continue;
}
if (((AttributeKey<?>) existing).getKey().equals(name)) {
data.set(i, key);
data.set(i + 1, limited);
return;
}
}
if (size >= countLimit) {
return;
}
if (emptySlot >= 0) {
data.set(emptySlot, key);
data.set(emptySlot + 1, limited);
} else {
data.add(key);
data.add(limited);
}
size++;
}

@SuppressWarnings("unchecked")
private void putValue(AttributeKey<?> key, Value<?> valueObj) {
// Convert VALUE type to narrower type when possible
Expand Down Expand Up @@ -111,8 +206,7 @@ private void putValue(AttributeKey<?> key, Value<?> valueObj) {
return;
case VALUE:
// Not coercible (empty, non-homogeneous, or unsupported element type)
data.add(key);
data.add(valueObj);
addPair(key, valueObj);
return;
default:
throw new IllegalArgumentException("Unexpected array attribute type: " + attributeType);
Expand All @@ -121,8 +215,7 @@ private void putValue(AttributeKey<?> key, Value<?> valueObj) {
case BYTES:
case EMPTY:
// Keep as VALUE type
data.add(key);
data.add(valueObj);
addPair(key, valueObj);
}
}

Expand Down Expand Up @@ -191,6 +284,8 @@ public AttributesBuilder removeIf(Predicate<AttributeKey<?>> predicate) {
// null items are filtered out in ArrayBackedAttributes
data.set(i, null);
data.set(i + 1, null);
size--;
cachedBuild = null;
}
}
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.common;

import com.google.auto.value.AutoValue;
import javax.annotation.concurrent.Immutable;

/**
* Limits enforced by an {@link AttributesBuilder} created via {@link
* Attributes#builder(AttributeLimits)}.
*
* <p>A builder configured with limits applies last-value-wins semantics on {@link
* AttributesBuilder#put put} (by {@link AttributeKey#getKey() key name}, regardless of {@link
* AttributeType}), truncates over-length string and byte values, replaces over-nested array and map
* values with empty containers, and drops entries added beyond the configured count limit. This
* differs from the default builder ({@link Attributes#builder()}) which defers de-duplication to
* {@link AttributesBuilder#build()} and applies no truncation, depth, or count limits.
*
* <p>The three parameters correspond to the {@code AttributeCountLimit}, {@code
* AttributeValueLengthLimit}, and {@code AttributeValueDepthLimit} configurable parameters in the
* OpenTelemetry <a
* href="https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/common#attribute-limits">common
* attribute-limits</a> specification.
*/
public abstract class AttributeLimits {

private static final AttributeLimits NO_LIMITS = new AttributeLimitsBuilder().build();

/** Returns an {@link AttributeLimits} that imposes no count, length, or depth limits. */
public static AttributeLimits noLimits() {
return NO_LIMITS;
}

/** Returns a new {@link AttributeLimitsBuilder} initialized to {@link #noLimits()}. */
public static AttributeLimitsBuilder builder() {
return new AttributeLimitsBuilder();
}

static AttributeLimits create(int countLimit, int valueLengthLimit, int valueDepthLimit) {
return new AutoValue_AttributeLimits_AttributeLimitsValue(
countLimit, valueLengthLimit, valueDepthLimit);
}

AttributeLimits() {}

/**
* Returns the maximum number of unique attribute keys ({@code AttributeCountLimit}). Additional
* entries with new key names are dropped once the limit is reached. Overwrites of existing keys
* do not consume against the limit.
*
* <p>{@link Integer#MAX_VALUE} means no count limit.
*/
public abstract int getCountLimit();

/**
* Returns the maximum length for string and byte-array attribute values ({@code
* AttributeValueLengthLimit}). Longer values are truncated to this length. Applies recursively to
* string and byte-array values within {@link Value}-typed and array attributes.
*
* <p>{@link Integer#MAX_VALUE} means no length limit.
*/
public abstract int getValueLengthLimit();

/**
* Returns the maximum nesting depth for array and map attribute values ({@code
* AttributeValueDepthLimit}). Depth counting starts at 1 for the top-level attribute value and
* increments when descending into array elements or map values. Arrays and maps at a depth
* greater than this limit are replaced with an empty container of the same shape.
*
* <p>{@link Integer#MAX_VALUE} means no depth limit.
*/
public abstract int getValueDepthLimit();

/**
* Returns an {@link AttributeLimitsBuilder} initialized to the same property values as this
* instance.
*/
public AttributeLimitsBuilder toBuilder() {
return builder()
.setCountLimit(getCountLimit())
.setValueLengthLimit(getValueLengthLimit())
.setValueDepthLimit(getValueDepthLimit());
}

@AutoValue
@Immutable
abstract static class AttributeLimitsValue extends AttributeLimits {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.common;

import static io.opentelemetry.api.internal.Utils.checkArgument;

/** Builder for {@link AttributeLimits}. */
public final class AttributeLimitsBuilder {

private int countLimit = Integer.MAX_VALUE;
private int valueLengthLimit = Integer.MAX_VALUE;

// TODO(jack-berg): before merging, decide whether to default this to 64 (spec-recommended,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to self

// matches System.Text.Json, provides stack safety when callers set a length limit but forget
// depth). Since depth is net new we can pick a non-infinite default without breaking anyone;
// count and length must stay at Integer.MAX_VALUE for back-compat. Would diverge builder
// defaults from AttributeLimits.noLimits().
private int valueDepthLimit = Integer.MAX_VALUE;

AttributeLimitsBuilder() {}

/**
* Sets the maximum number of unique attribute keys ({@code AttributeCountLimit}). Additional
* entries with new key names are dropped once the limit is reached. Overwrites of existing keys
* do not consume against the limit.
*
* @param countLimit non-negative maximum, or {@link Integer#MAX_VALUE} for no limit
* @throws IllegalArgumentException if {@code countLimit} is negative
*/
public AttributeLimitsBuilder setCountLimit(int countLimit) {
checkArgument(countLimit >= 0, "countLimit must be non-negative");
this.countLimit = countLimit;
return this;
}

/**
* Sets the maximum length for string and byte-array attribute values ({@code
* AttributeValueLengthLimit}). Applies recursively to string and byte-array values within {@link
* Value}-typed and array attributes.
*
* @param valueLengthLimit non-negative maximum, or {@link Integer#MAX_VALUE} for no limit
* @throws IllegalArgumentException if {@code valueLengthLimit} is negative
*/
public AttributeLimitsBuilder setValueLengthLimit(int valueLengthLimit) {
checkArgument(valueLengthLimit >= 0, "valueLengthLimit must be non-negative");
this.valueLengthLimit = valueLengthLimit;
return this;
}

/**
* Sets the maximum nesting depth for array and map attribute values ({@code
* AttributeValueDepthLimit}). Depth is 1-indexed (top-level attribute value = depth 1); the limit
* must therefore be at least 1. Arrays and maps at a depth greater than the limit are replaced
* with an empty container of the same shape.
*
* @param valueDepthLimit maximum nesting depth, minimum 1, or {@link Integer#MAX_VALUE} for no
* limit
* @throws IllegalArgumentException if {@code valueDepthLimit} is less than 1
*/
public AttributeLimitsBuilder setValueDepthLimit(int valueDepthLimit) {
checkArgument(valueDepthLimit >= 1, "valueDepthLimit must be at least 1");
this.valueDepthLimit = valueDepthLimit;
return this;
}

/** Builds the {@link AttributeLimits}. */
public AttributeLimits build() {
return AttributeLimits.create(countLimit, valueLengthLimit, valueDepthLimit);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ static AttributesBuilder builder() {
return new ArrayBackedAttributesBuilder();
}

/**
* Returns a new {@link AttributesBuilder} that enforces the given {@link AttributeLimits}.
* Passing {@link AttributeLimits#noLimits()} is equivalent to {@link #builder()}.
*/
static AttributesBuilder builder(AttributeLimits limits) {
return new ArrayBackedAttributesBuilder(limits);
}

/**
* Returns a new {@link AttributesBuilder} instance populated with the data of this {@link
* Attributes}.
Expand Down
Loading
Loading