Skip to content

Commit 70e8433

Browse files
authored
Add new configuration option to limit the size of string attribute values (#1484)
* Add new configuration option to limit the size of string attribute values * Polish * Polish * Polish * Polish * Add benchmark. * Format * Format
1 parent 6dffbb8 commit 70e8433

File tree

6 files changed

+279
-1
lines changed

6 files changed

+279
-1
lines changed

api/src/main/java/io/opentelemetry/internal/StringUtils.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package io.opentelemetry.internal;
1818

19+
import io.opentelemetry.common.AttributeValue;
20+
import java.util.List;
21+
import javax.annotation.Nullable;
1922
import javax.annotation.concurrent.Immutable;
2023

2124
/** Internal utility methods for working with attribute keys, attribute values, and metric names. */
@@ -57,5 +60,55 @@ public static boolean isValidMetricName(String metricName) {
5760
return metricName.matches(pattern);
5861
}
5962

63+
/**
64+
* If given attribute is of type STRING and has more characters than given {@code limit} then
65+
* return new AttributeValue with string truncated to {@code limit} characters.
66+
*
67+
* <p>If given attribute is of type STRING_ARRAY and non-empty then return new AttributeValue with
68+
* every element truncated to {@code limit} characters.
69+
*
70+
* <p>Otherwise return given {@code value}
71+
*
72+
* @throws IllegalArgumentException if limit is zero or negative
73+
*/
74+
public static AttributeValue truncateToSize(AttributeValue value, int limit) {
75+
Utils.checkArgument(limit > 0, "attribute value limit must be positive, got %d", limit);
76+
77+
if (value == null
78+
|| (value.getType() != AttributeValue.Type.STRING
79+
&& value.getType() != AttributeValue.Type.STRING_ARRAY)) {
80+
return value;
81+
}
82+
83+
if (value.getType() == AttributeValue.Type.STRING_ARRAY) {
84+
List<String> strings = value.getStringArrayValue();
85+
if (strings.isEmpty()) {
86+
return value;
87+
}
88+
89+
String[] newStrings = new String[strings.size()];
90+
for (int i = 0; i < strings.size(); i++) {
91+
String string = strings.get(i);
92+
newStrings[i] = truncateToSize(string, limit);
93+
}
94+
95+
return AttributeValue.arrayAttributeValue(newStrings);
96+
}
97+
98+
String string = value.getStringValue();
99+
// Don't allocate new AttributeValue if not needed
100+
return (string == null || string.length() <= limit)
101+
? value
102+
: AttributeValue.stringAttributeValue(string.substring(0, limit));
103+
}
104+
105+
@Nullable
106+
private static String truncateToSize(@Nullable String s, int limit) {
107+
if (s == null || s.length() <= limit) {
108+
return s;
109+
}
110+
return s.substring(0, limit);
111+
}
112+
60113
private StringUtils() {}
61114
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.sdk.trace;
18+
19+
import io.opentelemetry.sdk.OpenTelemetrySdk;
20+
import io.opentelemetry.sdk.trace.config.TraceConfig;
21+
import io.opentelemetry.trace.Span.Kind;
22+
import java.util.concurrent.TimeUnit;
23+
import org.openjdk.jmh.annotations.Benchmark;
24+
import org.openjdk.jmh.annotations.Fork;
25+
import org.openjdk.jmh.annotations.Level;
26+
import org.openjdk.jmh.annotations.Measurement;
27+
import org.openjdk.jmh.annotations.OutputTimeUnit;
28+
import org.openjdk.jmh.annotations.Param;
29+
import org.openjdk.jmh.annotations.Scope;
30+
import org.openjdk.jmh.annotations.Setup;
31+
import org.openjdk.jmh.annotations.State;
32+
import org.openjdk.jmh.annotations.Threads;
33+
import org.openjdk.jmh.annotations.Warmup;
34+
35+
@State(Scope.Benchmark)
36+
public class SpanAttributeTruncateBenchmark {
37+
38+
private final TracerSdk tracerSdk = OpenTelemetrySdk.getTracerProvider().get("benchmarkTracer");
39+
private SpanBuilderSdk spanBuilderSdk;
40+
41+
public String shortValue = "short";
42+
public String longValue = "very_long_attribute_and_then_some_more";
43+
public String veryLongValue;
44+
45+
@Param({"10", "1000000"})
46+
public int maxLength;
47+
48+
@Setup(Level.Trial)
49+
public final void setup() {
50+
TraceConfig config =
51+
OpenTelemetrySdk.getTracerProvider()
52+
.getActiveTraceConfig()
53+
.toBuilder()
54+
.setMaxLengthOfAttributeValues(maxLength)
55+
.build();
56+
OpenTelemetrySdk.getTracerProvider().updateActiveTraceConfig(config);
57+
spanBuilderSdk =
58+
(SpanBuilderSdk)
59+
tracerSdk
60+
.spanBuilder("benchmarkSpan")
61+
.setSpanKind(Kind.CLIENT)
62+
.setAttribute("key", "value");
63+
64+
String seed = "0123456789";
65+
StringBuilder longString = new StringBuilder();
66+
while (longString.length() < 10_000_000) {
67+
longString.append(seed);
68+
}
69+
veryLongValue = longString.toString();
70+
}
71+
72+
/** attributes that don't require any truncation. */
73+
@Benchmark
74+
@Threads(value = 1)
75+
@Fork(1)
76+
@Warmup(iterations = 5, time = 1)
77+
@Measurement(iterations = 10, time = 1)
78+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
79+
public RecordEventsReadableSpan shortAttributes() {
80+
RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilderSdk.startSpan();
81+
for (int i = 0; i < 10; i++) {
82+
span.setAttribute(String.valueOf(i), shortValue);
83+
}
84+
return span;
85+
}
86+
87+
/** even if we truncate, result is short. */
88+
@Benchmark
89+
@Threads(value = 1)
90+
@Fork(1)
91+
@Warmup(iterations = 5, time = 1)
92+
@Measurement(iterations = 10, time = 1)
93+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
94+
public RecordEventsReadableSpan longAttributes() {
95+
RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilderSdk.startSpan();
96+
for (int i = 0; i < 10; i++) {
97+
span.setAttribute(String.valueOf(i), longValue);
98+
}
99+
return span;
100+
}
101+
102+
/** have to copy very long strings. */
103+
@Benchmark
104+
@Threads(value = 1)
105+
@Fork(1)
106+
@Warmup(iterations = 5, time = 1)
107+
@Measurement(iterations = 10, time = 1)
108+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
109+
public RecordEventsReadableSpan veryLongAttributes() {
110+
RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilderSdk.startSpan();
111+
for (int i = 0; i < 10; i++) {
112+
span.setAttribute(String.valueOf(i), veryLongValue);
113+
}
114+
return span;
115+
}
116+
}

sdk/src/main/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpan.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.opentelemetry.common.Attributes;
2424
import io.opentelemetry.common.ReadableAttributes;
2525
import io.opentelemetry.common.ReadableKeyValuePairs.KeyValueConsumer;
26+
import io.opentelemetry.internal.StringUtils;
2627
import io.opentelemetry.sdk.common.Clock;
2728
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
2829
import io.opentelemetry.sdk.resources.Resource;
@@ -323,6 +324,11 @@ public void setAttribute(String key, AttributeValue value) {
323324
if (attributes == null) {
324325
attributes = new AttributesMap(traceConfig.getMaxNumberOfAttributes());
325326
}
327+
328+
if (traceConfig.shouldTruncateStringAttributeValues()) {
329+
value = StringUtils.truncateToSize(value, traceConfig.getMaxLengthOfAttributeValues());
330+
}
331+
326332
attributes.put(key, value);
327333
}
328334
}

sdk/src/main/java/io/opentelemetry/sdk/trace/SpanBuilderSdk.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.opentelemetry.common.Attributes;
2222
import io.opentelemetry.common.ReadableAttributes;
2323
import io.opentelemetry.common.ReadableKeyValuePairs.KeyValueConsumer;
24+
import io.opentelemetry.internal.StringUtils;
2425
import io.opentelemetry.internal.Utils;
2526
import io.opentelemetry.sdk.common.Clock;
2627
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
@@ -190,6 +191,11 @@ public Span.Builder setAttribute(String key, AttributeValue value) {
190191
if (attributes == null) {
191192
attributes = new AttributesMap(traceConfig.getMaxNumberOfAttributes());
192193
}
194+
195+
if (traceConfig.shouldTruncateStringAttributeValues()) {
196+
value = StringUtils.truncateToSize(value, traceConfig.getMaxLengthOfAttributeValues());
197+
}
198+
193199
attributes.put(key, value);
194200
return this;
195201
}

sdk/src/main/java/io/opentelemetry/sdk/trace/config/TraceConfig.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
* {@link Event}.
6060
* <li>{@code otel.config.max.link.attrs}: to set the global default max number of attributes per
6161
* {@link Link}.
62+
* <li>{@code otel.config.max.attr.length}: to set the global default max length of string
63+
* attribute value in characters.
6264
* </ul>
6365
*
6466
* <p>For environment variables, {@link TraceConfig} will look for the following names:
@@ -76,6 +78,8 @@
7678
* {@link Event}.
7779
* <li>{@code OTEL_CONFIG_MAX_LINK_ATTRS}: to set the global default max number of attributes per
7880
* {@link Link}.
81+
* <li>{@code OTEL_CONFIG_MAX_ATTR_LENGTH}: to set the global default max length of string
82+
* attribute value in characters.
7983
* </ul>
8084
*/
8185
@AutoValue
@@ -90,6 +94,9 @@ public abstract class TraceConfig {
9094
private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_EVENT = 32;
9195
private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK = 32;
9296

97+
public static final int UNLIMITED_ATTRIBUTE_LENGTH = -1;
98+
private static final int DEFAULT_MAX_ATTRIBUTE_LENGTH = UNLIMITED_ATTRIBUTE_LENGTH;
99+
93100
/**
94101
* Returns the default {@code TraceConfig}.
95102
*
@@ -144,6 +151,18 @@ public static TraceConfig getDefault() {
144151
*/
145152
public abstract int getMaxNumberOfAttributesPerLink();
146153

154+
/**
155+
* Returns the global default max length of string attribute value in characters.
156+
*
157+
* @return the global default max length of string attribute value in characters.
158+
* @see #shouldTruncateStringAttributeValues()
159+
*/
160+
public abstract int getMaxLengthOfAttributeValues();
161+
162+
public boolean shouldTruncateStringAttributeValues() {
163+
return getMaxLengthOfAttributeValues() != UNLIMITED_ATTRIBUTE_LENGTH;
164+
}
165+
147166
/**
148167
* Returns a new {@link Builder}.
149168
*
@@ -156,7 +175,8 @@ private static Builder newBuilder() {
156175
.setMaxNumberOfEvents(DEFAULT_SPAN_MAX_NUM_EVENTS)
157176
.setMaxNumberOfLinks(DEFAULT_SPAN_MAX_NUM_LINKS)
158177
.setMaxNumberOfAttributesPerEvent(DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_EVENT)
159-
.setMaxNumberOfAttributesPerLink(DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK);
178+
.setMaxNumberOfAttributesPerLink(DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK)
179+
.setMaxLengthOfAttributeValues(DEFAULT_MAX_ATTRIBUTE_LENGTH);
160180
}
161181

162182
/**
@@ -176,6 +196,7 @@ public abstract static class Builder extends ConfigBuilder<Builder> {
176196
private static final String KEY_SPAN_MAX_NUM_ATTRIBUTES_PER_EVENT =
177197
"otel.config.max.event.attrs";
178198
private static final String KEY_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK = "otel.config.max.link.attrs";
199+
private static final String KEY_SPAN_ATTRIBUTE_MAX_VALUE_LENGTH = "otel.config.max.attr.length";
179200

180201
Builder() {}
181202

@@ -214,6 +235,10 @@ protected Builder fromConfigMap(
214235
if (intValue != null) {
215236
this.setMaxNumberOfAttributesPerLink(intValue);
216237
}
238+
intValue = getIntProperty(KEY_SPAN_ATTRIBUTE_MAX_VALUE_LENGTH, configMap);
239+
if (intValue != null) {
240+
this.setMaxLengthOfAttributeValues(intValue);
241+
}
217242
return this;
218243
}
219244

@@ -325,6 +350,16 @@ public Builder setSamplerProbability(double samplerProbability) {
325350
*/
326351
public abstract Builder setMaxNumberOfAttributesPerLink(int maxNumberOfAttributesPerLink);
327352

353+
/**
354+
* Sets the global default max length of string attribute value in characters.
355+
*
356+
* @param maxLengthOfAttributeValues the global default max length of string attribute value in
357+
* characters. It must be non-negative (or {@link #UNLIMITED_ATTRIBUTE_LENGTH}) otherwise
358+
* {@link #build()} will throw an exception.
359+
* @return this.
360+
*/
361+
public abstract Builder setMaxLengthOfAttributeValues(int maxLengthOfAttributeValues);
362+
328363
abstract TraceConfig autoBuild();
329364

330365
/**
@@ -343,6 +378,8 @@ public TraceConfig build() {
343378
traceConfig.getMaxNumberOfAttributesPerEvent() > 0, "maxNumberOfAttributesPerEvent");
344379
Preconditions.checkArgument(
345380
traceConfig.getMaxNumberOfAttributesPerLink() > 0, "maxNumberOfAttributesPerLink");
381+
Preconditions.checkArgument(
382+
traceConfig.getMaxLengthOfAttributeValues() >= -1, "maxLengthOfAttributeValues");
346383
return traceConfig;
347384
}
348385
}

sdk/src/test/java/io/opentelemetry/sdk/trace/SpanBuilderSdkTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,66 @@ void droppingAttributes() {
414414
}
415415
}
416416

417+
@Test
418+
public void tooLargeAttributeValuesAreTruncated() {
419+
TraceConfig traceConfig =
420+
tracerSdkFactory
421+
.getActiveTraceConfig()
422+
.toBuilder()
423+
.setMaxLengthOfAttributeValues(10)
424+
.build();
425+
tracerSdkFactory.updateActiveTraceConfig(traceConfig);
426+
Span.Builder spanBuilder = tracerSdk.spanBuilder(SPAN_NAME);
427+
spanBuilder.setAttribute("builderStringNull", (String) null);
428+
spanBuilder.setAttribute("builderStringSmall", "small");
429+
spanBuilder.setAttribute("builderStringLarge", "very large string that we have to cut");
430+
spanBuilder.setAttribute("builderLong", 42L);
431+
spanBuilder.setAttribute(
432+
"builderStringLargeValue",
433+
AttributeValue.stringAttributeValue("very large string that we have to cut"));
434+
spanBuilder.setAttribute(
435+
"builderStringArray",
436+
AttributeValue.arrayAttributeValue("small", null, "very large string that we have to cut"));
437+
438+
RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan();
439+
span.setAttribute("spanStringSmall", "small");
440+
span.setAttribute("spanStringLarge", "very large string that we have to cut");
441+
span.setAttribute("spanLong", 42L);
442+
span.setAttribute(
443+
"spanStringLarge",
444+
AttributeValue.stringAttributeValue("very large string that we have to cut"));
445+
span.setAttribute(
446+
"spanStringArray",
447+
AttributeValue.arrayAttributeValue("small", null, "very large string that we have to cut"));
448+
449+
try {
450+
ReadableAttributes attrs = span.toSpanData().getAttributes();
451+
assertThat(attrs.get("builderStringNull")).isEqualTo(null);
452+
assertThat(attrs.get("builderStringSmall"))
453+
.isEqualTo(AttributeValue.stringAttributeValue("small"));
454+
assertThat(attrs.get("builderStringLarge"))
455+
.isEqualTo(AttributeValue.stringAttributeValue("very large"));
456+
assertThat(attrs.get("builderLong")).isEqualTo(AttributeValue.longAttributeValue(42L));
457+
assertThat(attrs.get("builderStringLargeValue"))
458+
.isEqualTo(AttributeValue.stringAttributeValue("very large"));
459+
assertThat(attrs.get("builderStringArray"))
460+
.isEqualTo(AttributeValue.arrayAttributeValue("small", null, "very large"));
461+
462+
assertThat(attrs.get("spanStringSmall"))
463+
.isEqualTo(AttributeValue.stringAttributeValue("small"));
464+
assertThat(attrs.get("spanStringLarge"))
465+
.isEqualTo(AttributeValue.stringAttributeValue("very large"));
466+
assertThat(attrs.get("spanLong")).isEqualTo(AttributeValue.longAttributeValue(42L));
467+
assertThat(attrs.get("spanStringLarge"))
468+
.isEqualTo(AttributeValue.stringAttributeValue("very large"));
469+
assertThat(attrs.get("spanStringArray"))
470+
.isEqualTo(AttributeValue.arrayAttributeValue("small", null, "very large"));
471+
} finally {
472+
span.end();
473+
tracerSdkFactory.updateActiveTraceConfig(TraceConfig.getDefault());
474+
}
475+
}
476+
417477
@Test
418478
void addAttributes_OnlyViaSampler() {
419479
TraceConfig traceConfig =

0 commit comments

Comments
 (0)