Skip to content

feat: add support for OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT #3090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions opentelemetry-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- *Fix* SpanProcessor::on_start is no longer called on non recording spans
- **Fix**: Restore true parallel exports in the async-native `BatchSpanProcessor` by honoring `OTEL_BSP_MAX_CONCURRENT_EXPORTS` ([#2959](https://github.com/open-telemetry/opentelemetry-rust/pull/3028)). A regression in [#2685](https://github.com/open-telemetry/opentelemetry-rust/pull/2685) inadvertently awaited the `export()` future directly in `opentelemetry-sdk/src/trace/span_processor_with_async_runtime.rs` instead of spawning it on the runtime, forcing all exports to run sequentially.
- **Feature**: Added `Clone` implementation to `SdkLogger` for API consistency with `SdkTracer` ([#3058](https://github.com/open-telemetry/opentelemetry-rust/issues/3058)).
- Add support for max span attribute value length; Applies to `Value::String` and `Array::String` only.

## 0.30.0

Expand Down
7 changes: 7 additions & 0 deletions opentelemetry-sdk/src/trace/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ impl Default for Config {
config.span_limits.max_attributes_per_span = max_attributes_per_span;
}

if let Some(max_attribute_value_length) = env::var("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT")
.ok()
.and_then(|count_limit| i32::from_str(&count_limit).ok())
{
config.span_limits.max_attribute_value_length = max_attribute_value_length;
}

if let Some(max_events_per_span) = env::var("OTEL_SPAN_EVENT_COUNT_LIMIT")
.ok()
.and_then(|max_events| u32::from_str(&max_events).ok())
Expand Down
9 changes: 9 additions & 0 deletions opentelemetry-sdk/src/trace/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,15 @@ impl TracerProviderBuilder {
self
}

/// Specify the maximum allowed length of attribute values.
Copy link
Member

Choose a reason for hiding this comment

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

could you enhance the doc to make it clear that this is applicable to string values or array of strings.
and also mention that the trimming behavior is simply trimming!
(some people might expect "..." being added as the last 3 characters to indicate trimming has occurred.. But we are not doing that, and there is no way a backend can tell if trimming has occurred. So if a user opts in to this, they should be made aware of the implications)

///
/// This limit applies to [Value::String](opentelemetry::Value::String) and [Array::String](opentelemetry::Array::String)
/// attributes only. When the limit is exceeded, the value is truncated without any indication that truncation has occurred.
pub fn with_max_attribute_value_length(mut self, max_attribute_value_length: u32) -> Self {
self.config.span_limits.max_attribute_value_length = max_attribute_value_length as i32;
self
}

/// Specify the number of events to be recorded per span.
pub fn with_max_links_per_span(mut self, max_links: u32) -> Self {
self.config.span_limits.max_links_per_span = max_links;
Expand Down
61 changes: 59 additions & 2 deletions opentelemetry-sdk/src/trace/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ impl opentelemetry::trace::Span for Span {
let dropped_attributes_count =
attributes.len().saturating_sub(event_attributes_limit);
attributes.truncate(event_attributes_limit);

data.events.add_event(Event::new(
name,
timestamp,
Expand Down Expand Up @@ -134,10 +133,16 @@ impl opentelemetry::trace::Span for Span {
/// Note that the OpenTelemetry project documents certain ["standard
/// attributes"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md)
/// that have prescribed semantic meanings.
fn set_attribute(&mut self, attribute: KeyValue) {
fn set_attribute(&mut self, mut attribute: KeyValue) {
let span_attribute_limit = self.span_limits.max_attributes_per_span as usize;
let span_attribute_value_limit = self.span_limits.max_attribute_value_length;
self.with_data(|data| {
if data.attributes.len() < span_attribute_limit {
if span_attribute_value_limit > -1 {
attribute
.value
.truncate(span_attribute_value_limit as usize);
}
data.attributes.push(attribute);
} else {
data.dropped_attributes_count += 1;
Expand Down Expand Up @@ -278,6 +283,7 @@ mod tests {
use crate::trace::{SpanEvents, SpanLinks};
use opentelemetry::trace::{self, SpanBuilder, TraceFlags, TraceId, Tracer};
use opentelemetry::{trace::Span as _, trace::TracerProvider};
use opentelemetry::{Array, StringValue, Value};
use std::time::Duration;
use std::vec;

Expand Down Expand Up @@ -566,6 +572,57 @@ mod tests {
);
}

#[test]
fn exceed_span_attributes_value_length_limit() {
let max_attribute_value_length = 4;
let exporter = NoopSpanExporter::new();
let provider_builder = crate::trace::SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.with_max_attribute_value_length(max_attribute_value_length);
let provider = provider_builder.build();
let tracer = provider.tracer("opentelemetry-test");

let initial_attributes = vec![
KeyValue::new("first", String::from("test")),
KeyValue::new("second", String::from("test data")),
KeyValue::new(
"third",
Value::Array(
[
StringValue::from("t"),
StringValue::from("test"),
StringValue::from("test data"),
StringValue::from("test data, once again"),
]
.to_vec()
.into(),
),
),
];
let span_builder = SpanBuilder::from_name("test_span").with_attributes(initial_attributes);

let mut span = tracer.build(span_builder);
span.set_attribute(KeyValue::new("fourth", "test data, after span builder"));

span.with_data(|data| {
for attribute in &data.attributes {
if let Value::String(_) = &attribute.value {
assert!(
attribute.value.as_str().len() <= max_attribute_value_length as usize,
"Span attribute values should be truncated to the max length"
);
} else if let Value::Array(Array::String(elems)) = &attribute.value {
for elem in elems {
assert!(
elem.as_str().len() <= max_attribute_value_length as usize,
"Span attribute values should be truncated to the max length"
);
}
}
}
});
}

#[test]
fn exceed_event_attributes_limit() {
let exporter = NoopSpanExporter::new();
Expand Down
4 changes: 4 additions & 0 deletions opentelemetry-sdk/src/trace/span_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
/// index in the collection. The one added to collections later will be dropped first.
pub(crate) const DEFAULT_MAX_EVENT_PER_SPAN: u32 = 128;
pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_SPAN: u32 = 128;
pub(crate) const DEFAULT_MAX_ATTRIBUTE_VALUE_LENGTH: i32 = -1;
pub(crate) const DEFAULT_MAX_LINKS_PER_SPAN: u32 = 128;
pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_EVENT: u32 = 128;
pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_LINK: u32 = 128;
Expand All @@ -25,6 +26,8 @@ pub struct SpanLimits {
pub max_events_per_span: u32,
/// The max attributes that can be added to a `Span`.
pub max_attributes_per_span: u32,
/// The max length of attribute values added to a `Span`.
pub max_attribute_value_length: i32,
/// The max links that can be added to a `Span`.
pub max_links_per_span: u32,
/// The max attributes that can be added into an `Event`
Expand All @@ -38,6 +41,7 @@ impl Default for SpanLimits {
SpanLimits {
max_events_per_span: DEFAULT_MAX_EVENT_PER_SPAN,
max_attributes_per_span: DEFAULT_MAX_ATTRIBUTES_PER_SPAN,
max_attribute_value_length: DEFAULT_MAX_ATTRIBUTE_VALUE_LENGTH,
max_links_per_span: DEFAULT_MAX_LINKS_PER_SPAN,
max_attributes_per_link: DEFAULT_MAX_ATTRIBUTES_PER_LINK,
max_attributes_per_event: DEFAULT_MAX_ATTRIBUTES_PER_EVENT,
Expand Down
8 changes: 8 additions & 0 deletions opentelemetry-sdk/src/trace/tracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ impl SdkTracer {
.saturating_sub(span_attributes_limit);
attribute_options.truncate(span_attributes_limit);
let dropped_attributes_count = dropped_attributes_count as u32;
let span_attribute_value_limit = span_limits.max_attribute_value_length;
Copy link
Member

Choose a reason for hiding this comment

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

set_attribute already does the truncation...Is it needed to be done again on this method?

Copy link
Author

@msierks-pcln msierks-pcln Jul 28, 2025

Choose a reason for hiding this comment

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

From what I can tell, this method is used by the SpanBuilder when a span is initially built. The set_attribute method is used for setting single attributes after the span has been created.

if span_attribute_value_limit > -1 {
for attribute in attribute_options.iter_mut() {
attribute
.value
.truncate(span_attribute_value_limit as usize);
}
}

// Links are available as Option<Vec<Link>> in the builder
// If it is None, then there are no links to process.
Expand Down
113 changes: 112 additions & 1 deletion opentelemetry/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,31 @@ impl StringValue {
pub fn as_str(&self) -> &str {
self.0.as_str()
}

/// Shortens this `StringValue` to the specified character length.
pub fn truncate(&mut self, new_len: usize) {
let s = self.as_ref();
if s.len() > new_len {
let new_value = truncate_str(s, new_len).to_string();

match &self.0 {
OtelString::Owned(_) | OtelString::Static(_) => {
self.0 = OtelString::Owned(new_value.into_boxed_str());
}
OtelString::RefCounted(_) => {
self.0 = OtelString::RefCounted(Arc::from(new_value.as_str()))
}
}
}
}
}

/// Returns a str truncated to the specified character length.
fn truncate_str(s: &str, new_len: usize) -> &str {
match s.char_indices().nth(new_len) {
None => s,
Some((idx, _)) => &s[..idx],
}
}

impl From<StringValue> for String {
Expand Down Expand Up @@ -327,6 +352,17 @@ impl Value {
Value::Array(v) => format!("{v}").into(),
}
}

/// Shortens this `Value` to the specified length.
///
/// Only [Value::String] and [Array::String] values are truncated.
pub fn truncate(&mut self, new_len: usize) {
match self {
Value::Array(Array::String(v)) => v.iter_mut().for_each(|s| s.truncate(new_len)),
Value::String(s) => s.truncate(new_len),
_ => (),
}
}
}

macro_rules! from_values {
Expand Down Expand Up @@ -628,7 +664,7 @@ impl InstrumentationScopeBuilder {
mod tests {
use std::hash::{Hash, Hasher};

use crate::{InstrumentationScope, KeyValue};
use crate::{Array, InstrumentationScope, KeyValue, Value};

use rand::random;
use std::collections::hash_map::DefaultHasher;
Expand Down Expand Up @@ -695,6 +731,81 @@ mod tests {
}
}

#[test]
fn value_truncate() {
for test in [
(0, Value::from(true), Value::from(true)),
(
0,
Value::Array(Array::Bool(vec![true, false])),
Value::Array(Array::Bool(vec![true, false])),
),
(0, Value::from(42), Value::from(42)),
(
0,
Value::Array(Array::I64(vec![42, -1])),
Value::Array(Array::I64(vec![42, -1])),
),
(0, Value::from(42.0), Value::from(42.0)),
(
0,
Value::Array(Array::F64(vec![42.0, -1.0])),
Value::Array(Array::F64(vec![42.0, -1.0])),
),
(0, Value::from("value"), Value::from("")),
(
0,
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
Value::Array(Array::String(vec!["".into(), "".into()])),
),
(1, Value::from("value"), Value::from("v")),
(
1,
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
Value::Array(Array::String(vec!["v".into(), "v".into()])),
),
(5, Value::from("value"), Value::from("value")),
(
7,
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
),
(
6,
Value::Array(Array::String(vec!["value".into(), "value-1".into()])),
Value::Array(Array::String(vec!["value".into(), "value-".into()])),
),
(128, Value::from("value"), Value::from("value")),
(
128,
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
Value::Array(Array::String(vec!["value-0".into(), "value-1".into()])),
),
]
.iter_mut()
{
test.1.truncate(test.0);
assert_eq!(test.1, test.2)
}
}

#[test]
fn truncate_str() {
for test in [
(5, "", ""),
(0, "Zero", ""),
(10, "Short text", "Short text"),
(1, "Hello World!", "H"),
(6, "hello€€", "hello€"),
(2, "££££££", "££"),
(8, "hello€world", "hello€wo"),
]
.iter()
{
assert_eq!(super::truncate_str(test.1, test.0), test.2);
}
}

fn hash_helper<T: Hash>(item: &T) -> u64 {
let mut hasher = DefaultHasher::new();
item.hash(&mut hasher);
Expand Down
Loading