From a2dfde0ee9d14aab634d8a3ba2306d3836b3de72 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 29 Jan 2026 13:30:50 +0100 Subject: [PATCH 01/19] Set span attributes --- relay-event-schema/src/protocol/span_v2.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/relay-event-schema/src/protocol/span_v2.rs b/relay-event-schema/src/protocol/span_v2.rs index 5aa9a028a45..10db149b6c5 100644 --- a/relay-event-schema/src/protocol/span_v2.rs +++ b/relay-event-schema/src/protocol/span_v2.rs @@ -11,41 +11,43 @@ use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId}; #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] pub struct SpanV2 { /// The ID of the trace to which this span belongs. - #[metastructure(required = true, nonempty = true, trim = false)] + #[metastructure(required = true, nonempty = true, trim = false, bytes_size = 0)] pub trace_id: Annotated, /// The ID of the span enclosing this span. + #[metastructure(trim = false, bytes_size = 0)] pub parent_span_id: Annotated, /// The Span ID. - #[metastructure(required = true, nonempty = true, trim = false)] + #[metastructure(required = true, nonempty = true, trim = false, bytes_size = 0)] pub span_id: Annotated, /// Span type (see `OperationType` docs). - #[metastructure(required = true)] + #[metastructure(required = true, bytes_size = 0)] pub name: Annotated, /// The span's status. - #[metastructure(required = true)] + #[metastructure(required = true, bytes_size = 0)] pub status: Annotated, /// Whether this span is the root span of a segment. + #[metastructure(trim = false, bytes_size = 0)] pub is_segment: Annotated, /// Timestamp when the span started. - #[metastructure(required = true)] + #[metastructure(required = true, trim = false, bytes_size = 0)] pub start_timestamp: Annotated, /// Timestamp when the span was ended. - #[metastructure(required = true)] + #[metastructure(required = true, trim = false, bytes_size = 0)] pub end_timestamp: Annotated, /// Links from this span to other spans. - #[metastructure(pii = "maybe")] + #[metastructure(pii = "maybe", trim = true)] pub links: Annotated>, /// Arbitrary attributes on a span. - #[metastructure(pii = "true", trim = false)] + #[metastructure(pii = "true", trim = true)] pub attributes: Annotated, /// Additional arbitrary fields for forwards compatibility. From 218c21be13e189b996a3dafa6bd2b122280c437b Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 29 Jan 2026 13:31:13 +0100 Subject: [PATCH 02/19] Add default removed_key_byte_budget constant --- relay-event-normalization/src/eap/trimming.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/relay-event-normalization/src/eap/trimming.rs b/relay-event-normalization/src/eap/trimming.rs index b5eac1b8e3d..4ba8bad0df9 100644 --- a/relay-event-normalization/src/eap/trimming.rs +++ b/relay-event-normalization/src/eap/trimming.rs @@ -9,6 +9,11 @@ use relay_protocol::{Array, Empty, Meta, Object}; use crate::eap::size; +/// Default value for a [`TrimmingProcessor`]'s `removed_key_byte_budget` field. +/// +/// This determines the limit up to which the keys of discarded items will be kept. +pub const REMOVED_KEY_BYTE_BUDGET: usize = 10 * 1024; + #[derive(Clone, Debug)] struct SizeState { max_depth: Option, From 13df0cf5e3625b0c5ed35b85bde1a238d882ee25 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 29 Jan 2026 13:31:37 +0100 Subject: [PATCH 03/19] Switch trimming processor for spans --- relay-event-normalization/src/eap/mod.rs | 2 +- relay-server/src/processing/spans/process.rs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/relay-event-normalization/src/eap/mod.rs b/relay-event-normalization/src/eap/mod.rs index b3b06698c7e..2a01aa9db92 100644 --- a/relay-event-normalization/src/eap/mod.rs +++ b/relay-event-normalization/src/eap/mod.rs @@ -26,11 +26,11 @@ mod ai; mod size; pub mod time; pub mod trace_metric; -#[allow(unused)] mod trimming; pub use self::ai::normalize_ai; pub use self::size::*; +pub use self::trimming::{REMOVED_KEY_BYTE_BUDGET, TrimmingProcessor}; /// Infers the sentry.op attribute and inserts it into [`Attributes`] if not already set. pub fn normalize_sentry_op(attributes: &mut Annotated) { diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index 49e8395a686..3431a512dad 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -1,9 +1,7 @@ use std::collections::BTreeMap; use std::time::Duration; -use relay_event_normalization::{ - GeoIpLookup, RequiredMode, SchemaProcessor, TrimmingProcessor, eap, -}; +use relay_event_normalization::{GeoIpLookup, RequiredMode, SchemaProcessor, eap}; use relay_event_schema::processor::{ProcessingState, ValueType, process_value}; use relay_event_schema::protocol::{Span, SpanId, SpanV2}; use relay_protocol::Annotated; @@ -190,7 +188,12 @@ fn normalize_span( eap::write_legacy_attributes(&mut span.attributes); }; - process_value(span, &mut TrimmingProcessor::new(), ProcessingState::root())?; + process_value( + span, + &mut eap::TrimmingProcessor::new(eap::REMOVED_KEY_BYTE_BUDGET), + // TODO: Provide the total item size limit + ProcessingState::root(), + )?; process_value( span, &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent), From e744635d9d087a530652e5ea43ab422501fcacbf Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 12:21:03 +0100 Subject: [PATCH 04/19] Add comment to trimming processor --- relay-event-normalization/src/eap/trimming.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/relay-event-normalization/src/eap/trimming.rs b/relay-event-normalization/src/eap/trimming.rs index 4ba8bad0df9..2c6ef54709f 100644 --- a/relay-event-normalization/src/eap/trimming.rs +++ b/relay-event-normalization/src/eap/trimming.rs @@ -364,6 +364,12 @@ impl Processor for TrimmingProcessor { return Ok(()); } + // This counts the lengths of all attribute keys regardless of whether + // the attribute itself is valid or invalid. Strictly speaking, this is + // inconsistent with the trimming logic, which only counts keys of valid + // attributes. However, this value is only used to set the `original_value` + // on the attributes collection for documentation purposes, we accept this + // discrepancy for now. In any case this is fine to change. let original_length = size::attributes_size(attributes); // Sort attributes by key + value size so small attributes are more likely to be preserved. From a7771b951ae2f3c3b43efffd841733f6052a3546 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 12:21:34 +0100 Subject: [PATCH 05/19] Add trimming config to project config --- relay-dynamic-config/src/project.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/relay-dynamic-config/src/project.rs b/relay-dynamic-config/src/project.rs index bf1d409d3ee..79d823e2171 100644 --- a/relay-dynamic-config/src/project.rs +++ b/relay-dynamic-config/src/project.rs @@ -50,6 +50,9 @@ pub struct ProjectConfig { /// Retention settings for different products. #[serde(default, skip_serializing_if = "RetentionsConfig::is_empty")] pub retentions: RetentionsConfig, + /// Trimming settings for different products. + #[serde(default, skip_serializing_if = "TrimmingConfigs::is_empty")] + pub trimming: TrimmingConfigs, /// Usage quotas for this project. #[serde(skip_serializing_if = "Vec::is_empty")] pub quotas: Vec, @@ -159,6 +162,7 @@ impl Default for ProjectConfig { event_retention: None, downsampled_event_retention: None, retentions: Default::default(), + trimming: Default::default(), quotas: Vec::new(), sampling: None, measurements: None, @@ -205,6 +209,8 @@ pub struct LimitedProjectConfig { pub filter_settings: ProjectFiltersConfig, #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")] pub datascrubbing_settings: DataScrubbingConfig, + #[serde(skip_serializing_if = "TrimmingConfigs::is_empty")] + pub trimming: TrimmingConfigs, #[serde(skip_serializing_if = "Option::is_none")] pub sampling: Option>, #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")] @@ -277,6 +283,29 @@ impl RetentionsConfig { } } +/// Per-category settings for item trimming. +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct TrimmingConfig { + /// The maximum size in bytes above which an item should be trimmed. + pub max_bytes: u32, +} + +/// Settings for item trimming. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct TrimmingConfigs { + /// Trimming settings for spans. + #[serde(skip_serializing_if = "Option::is_none")] + pub span: Option, +} + +impl TrimmingConfigs { + fn is_empty(&self) -> bool { + let Self { span } = self; + span.is_none() + } +} + fn is_false(value: &bool) -> bool { !*value } From 23046db3729b997c599921b4cb98691158d33354 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 12:22:04 +0100 Subject: [PATCH 06/19] Pass trimming config to span trimming --- relay-server/src/processing/spans/process.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index 3431a512dad..e9358626b4a 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -1,8 +1,9 @@ +use std::borrow::Cow; use std::collections::BTreeMap; use std::time::Duration; use relay_event_normalization::{GeoIpLookup, RequiredMode, SchemaProcessor, eap}; -use relay_event_schema::processor::{ProcessingState, ValueType, process_value}; +use relay_event_schema::processor::{FieldAttrs, ProcessingState, ValueType, process_value}; use relay_event_schema::protocol::{Span, SpanId, SpanV2}; use relay_protocol::Annotated; @@ -188,12 +189,22 @@ fn normalize_span( eap::write_legacy_attributes(&mut span.attributes); }; + // Set a max_bytes value on the root state if it's defined in the project config. + // This causes the whole item to be trimmed down to the limit. + let trimming_root_state = { + let mut attrs = FieldAttrs::default(); + if let Some(span_config) = ctx.project_info.config().trimming.span { + attrs = attrs.max_bytes(span_config.max_bytes as usize); + } + ProcessingState::new_root(Some(Cow::Owned(attrs)), []) + }; + process_value( span, &mut eap::TrimmingProcessor::new(eap::REMOVED_KEY_BYTE_BUDGET), - // TODO: Provide the total item size limit - ProcessingState::root(), + &trimming_root_state, )?; + process_value( span, &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent), From 223cc10ea5a69382f6c45e32900d4f61f4ea3f1e Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 12:43:32 +0100 Subject: [PATCH 07/19] Add an integration test --- tests/integration/test_spansv2.py | 182 ++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 2aea511d69b..91c3f85779f 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -173,6 +173,188 @@ def test_spansv2_basic( ] +def test_spansv2_trimming_basic( + mini_sentry, + relay, + relay_with_processing, + spans_consumer, + metrics_consumer, +): + """ + An adaptation of `test_spansv2_basic` that has a size limit for spans and attributes large enough + to demonstrate that trimming works. + """ + spans_consumer = spans_consumer() + metrics_consumer = metrics_consumer() + + project_id = 42 + project_config = mini_sentry.add_full_project_config(project_id) + project_config["config"].update( + { + "features": [ + "organizations:standalone-span-ingestion", + "projects:span-v2-experimental-processing", + ], + "retentions": {"span": {"standard": 42, "downsampled": 1337}}, + # This is sufficient for all builtin attributes not + # to be trimmed. + "trimming": {"span": {"max_bytes": 510}}, + } + ) + + relay = relay(relay_with_processing(options=TEST_CONFIG), options=TEST_CONFIG) + + ts = datetime.now(timezone.utc) + envelope = envelope_with_spans( + { + "start_timestamp": ts.timestamp(), + "end_timestamp": ts.timestamp() + 0.5, + "trace_id": "5b8efff798038103d269b633813fc60c", + "span_id": "eee19b7ec3c1b175", + "is_segment": True, + "name": "some op", + "status": "ok", + "attributes": { + "custom.string.attribute": { + "value": "This is actually a pretty long string", + "type": "string", + }, + # This attribute will get trimmed in the middle of the third string. + "custom.array.attribute": { + "value": [ + "A string", + "Another longer string", + "Yet another string", + ], + "type": "array", + }, + "custom.invalid.attribute": {"value": True, "type": "string"}, + "http.response_content_length": {"value": 17, "type": "integer"}, + }, + }, + trace_info={ + "trace_id": "5b8efff798038103d269b633813fc60c", + "public_key": project_config["publicKeys"][0]["publicKey"], + "release": "foo@1.0", + "environment": "prod", + "transaction": "/my/fancy/endpoint", + }, + ) + + relay.send_envelope(project_id, envelope) + + span = spans_consumer.get_span() + print(span["attributes"]) + + assert span == { + "trace_id": "5b8efff798038103d269b633813fc60c", + "span_id": "eee19b7ec3c1b175", + "attributes": { + "custom.array.attribute": { + "type": "array", + "value": ["A string", "Another longer string", "Yet anot..."], + }, + "custom.string.attribute": { + "type": "string", + "value": "This is actually a pretty long string", + }, + "http.response_content_length": {"value": 17, "type": "integer"}, + "http.response.body.size": {"value": 17, "type": "integer"}, + "custom.invalid.attribute": None, + "sentry.browser.name": {"type": "string", "value": "Python Requests"}, + "sentry.browser.version": {"type": "string", "value": "2.32"}, + "sentry.dsc.environment": {"type": "string", "value": "prod"}, + "sentry.dsc.public_key": { + "type": "string", + "value": project_config["publicKeys"][0]["publicKey"], + }, + "sentry.dsc.release": {"type": "string", "value": "foo@1.0"}, + "sentry.dsc.transaction": {"type": "string", "value": "/my/fancy/endpoint"}, + "sentry.dsc.trace_id": { + "type": "string", + "value": "5b8efff798038103d269b633813fc60c", + }, + "sentry.observed_timestamp_nanos": { + "type": "string", + "value": time_within(ts, expect_resolution="ns"), + }, + "sentry.op": {"type": "string", "value": "default"}, + }, + "_meta": { + "attributes": { + "": {"len": 541}, + "custom.array.attribute": { + "value": { + "2": { + "": { + "len": 18, + "rem": [ + [ + "!limit", + "s", + 8, + 11, + ], + ], + }, + }, + }, + }, + "custom.invalid.attribute": { + "": { + "err": ["invalid_data"], + "val": {"type": "string", "value": True}, + } + }, + } + }, + "name": "some op", + "received": time_within(ts), + "start_timestamp": time_is(ts), + "end_timestamp": time_is(ts.timestamp() + 0.5), + "is_segment": True, + "status": "ok", + "retention_days": 42, + "downsampled_retention_days": 1337, + "key_id": 123, + "organization_id": 1, + "project_id": 42, + } + + assert metrics_consumer.get_metrics(n=2, with_headers=False) == [ + { + "name": "c:spans/count_per_root_project@none", + "org_id": 1, + "project_id": 42, + "received_at": time_within(ts, precision="s"), + "retention_days": 90, + "tags": { + "decision": "keep", + "is_segment": "true", + "target_project_id": "42", + "transaction": "/my/fancy/endpoint", + }, + "timestamp": time_within_delta(), + "type": "c", + "value": 1.0, + }, + { + "name": "c:spans/usage@none", + "org_id": 1, + "project_id": 42, + "received_at": time_within(ts, precision="s"), + "retention_days": 90, + "tags": { + "was_transaction": "false", + "is_segment": "true", + }, + "timestamp": time_within_delta(), + "type": "c", + "value": 1.0, + }, + ] + + @pytest.mark.parametrize( "rule_type", ["project", "trace"], From db0dce6677ec96af3e6f89c4d1d598a1f18d587f Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 12:56:31 +0100 Subject: [PATCH 08/19] Replace constant by config option --- relay-config/src/config.rs | 19 +++++++++++++++++++ relay-event-normalization/src/eap/mod.rs | 2 +- relay-event-normalization/src/eap/trimming.rs | 5 ----- relay-server/src/processing/spans/process.rs | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 5697b03a9cb..9a682206b1f 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -700,6 +700,16 @@ pub struct Limits { /// /// Defaults to `1024`, a value [google has been using for a long time](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=19f92a030ca6d772ab44b22ee6a01378a8cb32d4). pub tcp_listen_backlog: u32, + /// The byte size limit up to which Relay will retain + /// keys of invalid/removed attributes. + /// + /// This is only relevant for EAP items (spans, logs, …). + /// In principle, we want to record all deletions of attributes, + /// but we have to institute some limit to protect our infrastructure + /// against excessive metadata sizes. + /// + /// Defaults to 10KiB. + pub max_removed_attribute_key_bytes: ByteSize, } impl Default for Limits { @@ -735,6 +745,7 @@ impl Default for Limits { idle_timeout: None, max_connections: None, tcp_listen_backlog: 1024, + max_removed_attribute_key_bytes: ByteSize::kibibytes(10), } } } @@ -2469,6 +2480,14 @@ impl Config { self.values.limits.max_concurrent_queries } + /// Returns the maximum combined size of keys of invalid attributes. + pub fn max_removed_attribute_key_bytes(&self) -> usize { + self.values + .limits + .max_removed_attribute_key_bytes + .as_bytes() + } + /// The maximum number of seconds a query is allowed to take across retries. pub fn query_timeout(&self) -> Duration { Duration::from_secs(self.values.limits.query_timeout) diff --git a/relay-event-normalization/src/eap/mod.rs b/relay-event-normalization/src/eap/mod.rs index 2a01aa9db92..84e2ec7ef5f 100644 --- a/relay-event-normalization/src/eap/mod.rs +++ b/relay-event-normalization/src/eap/mod.rs @@ -30,7 +30,7 @@ mod trimming; pub use self::ai::normalize_ai; pub use self::size::*; -pub use self::trimming::{REMOVED_KEY_BYTE_BUDGET, TrimmingProcessor}; +pub use self::trimming::TrimmingProcessor; /// Infers the sentry.op attribute and inserts it into [`Attributes`] if not already set. pub fn normalize_sentry_op(attributes: &mut Annotated) { diff --git a/relay-event-normalization/src/eap/trimming.rs b/relay-event-normalization/src/eap/trimming.rs index 2c6ef54709f..7dff55d1a17 100644 --- a/relay-event-normalization/src/eap/trimming.rs +++ b/relay-event-normalization/src/eap/trimming.rs @@ -9,11 +9,6 @@ use relay_protocol::{Array, Empty, Meta, Object}; use crate::eap::size; -/// Default value for a [`TrimmingProcessor`]'s `removed_key_byte_budget` field. -/// -/// This determines the limit up to which the keys of discarded items will be kept. -pub const REMOVED_KEY_BYTE_BUDGET: usize = 10 * 1024; - #[derive(Clone, Debug)] struct SizeState { max_depth: Option, diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index e9358626b4a..d041af6a4d0 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -201,7 +201,7 @@ fn normalize_span( process_value( span, - &mut eap::TrimmingProcessor::new(eap::REMOVED_KEY_BYTE_BUDGET), + &mut eap::TrimmingProcessor::new(ctx.config.max_removed_attribute_key_bytes()), &trimming_root_state, )?; From 7ce149da0e0a13cba9b934bba9e6b7f33aef7232 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 13:15:24 +0100 Subject: [PATCH 09/19] Adapt test --- tests/integration/test_spansv2.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 91c3f85779f..12d7acbe477 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -198,11 +198,18 @@ def test_spansv2_trimming_basic( "retentions": {"span": {"standard": 42, "downsampled": 1337}}, # This is sufficient for all builtin attributes not # to be trimmed. - "trimming": {"span": {"max_bytes": 510}}, + "trimming": {"span": {"max_bytes": 445}}, } ) - relay = relay(relay_with_processing(options=TEST_CONFIG), options=TEST_CONFIG) + config = { + "limits": { + "max_removed_attribute_key_bytes": 30, + }, + **TEST_CONFIG, + } + + relay = relay(relay_with_processing(options=config), options=config) ts = datetime.now(timezone.utc) envelope = envelope_with_spans( @@ -229,7 +236,9 @@ def test_spansv2_trimming_basic( "type": "array", }, "custom.invalid.attribute": {"value": True, "type": "string"}, - "http.response_content_length": {"value": 17, "type": "integer"}, + # This attribute will be removed because the `max_removed_attribute_key_bytes` (30B) + # is already consumed by the previous invalid attribute + "second.custom.invalid.attribute": {"value": None, "type": "integer"}, }, }, trace_info={ @@ -252,14 +261,12 @@ def test_spansv2_trimming_basic( "attributes": { "custom.array.attribute": { "type": "array", - "value": ["A string", "Another longer string", "Yet anot..."], + "value": ["A string", "Another longer string", "Yet anothe..."], }, "custom.string.attribute": { "type": "string", "value": "This is actually a pretty long string", }, - "http.response_content_length": {"value": 17, "type": "integer"}, - "http.response.body.size": {"value": 17, "type": "integer"}, "custom.invalid.attribute": None, "sentry.browser.name": {"type": "string", "value": "Python Requests"}, "sentry.browser.version": {"type": "string", "value": "2.32"}, @@ -282,7 +289,7 @@ def test_spansv2_trimming_basic( }, "_meta": { "attributes": { - "": {"len": 541}, + "": {"len": 505}, "custom.array.attribute": { "value": { "2": { @@ -292,8 +299,8 @@ def test_spansv2_trimming_basic( [ "!limit", "s", - 8, - 11, + 10, + 13, ], ], }, From a972a124bee3ee45858e7852098092913a635875 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 13:37:02 +0100 Subject: [PATCH 10/19] Set fields on span links --- relay-event-schema/src/protocol/span_v2.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/relay-event-schema/src/protocol/span_v2.rs b/relay-event-schema/src/protocol/span_v2.rs index 10db149b6c5..89e283b02cf 100644 --- a/relay-event-schema/src/protocol/span_v2.rs +++ b/relay-event-schema/src/protocol/span_v2.rs @@ -167,23 +167,23 @@ impl IntoValue for SpanV2Status { #[metastructure(trim = false)] pub struct SpanV2Link { /// The trace id of the linked span. - #[metastructure(required = true, trim = false)] + #[metastructure(required = true, trim = false, bytes_size = 0)] pub trace_id: Annotated, /// The span id of the linked span. - #[metastructure(required = true, trim = false)] + #[metastructure(required = true, trim = false, bytes_size = 0)] pub span_id: Annotated, /// Whether the linked span was positively/negatively sampled. - #[metastructure(trim = false)] + #[metastructure(trim = false, bytes_size = 0)] pub sampled: Annotated, /// Span link attributes, similar to span attributes/data. - #[metastructure(pii = "maybe", trim = false)] + #[metastructure(pii = "maybe", trim = true)] pub attributes: Annotated, /// Additional arbitrary fields for forwards compatibility. - #[metastructure(additional_properties, pii = "maybe", trim = false)] + #[metastructure(additional_properties, pii = "maybe")] pub other: Object, } From 8dd2dc5983fe56e3ba5ffdb99fbefd70cf710f9c Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 13:51:15 +0100 Subject: [PATCH 11/19] Leftover print --- tests/integration/test_spansv2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 12d7acbe477..329d0a13f22 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -253,7 +253,6 @@ def test_spansv2_trimming_basic( relay.send_envelope(project_id, envelope) span = spans_consumer.get_span() - print(span["attributes"]) assert span == { "trace_id": "5b8efff798038103d269b633813fc60c", From eec350c538bbcf1546dc2affd1991728e8be1a9e Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 18:10:19 +0100 Subject: [PATCH 12/19] Rename _bytes options to _size --- relay-config/src/config.rs | 11 ++++------- relay-dynamic-config/src/project.rs | 2 +- relay-server/src/processing/spans/process.rs | 4 ++-- tests/integration/test_spansv2.py | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 9a682206b1f..0cb9bdb960d 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -709,7 +709,7 @@ pub struct Limits { /// against excessive metadata sizes. /// /// Defaults to 10KiB. - pub max_removed_attribute_key_bytes: ByteSize, + pub max_removed_attribute_key_size: ByteSize, } impl Default for Limits { @@ -745,7 +745,7 @@ impl Default for Limits { idle_timeout: None, max_connections: None, tcp_listen_backlog: 1024, - max_removed_attribute_key_bytes: ByteSize::kibibytes(10), + max_removed_attribute_key_size: ByteSize::kibibytes(10), } } } @@ -2481,11 +2481,8 @@ impl Config { } /// Returns the maximum combined size of keys of invalid attributes. - pub fn max_removed_attribute_key_bytes(&self) -> usize { - self.values - .limits - .max_removed_attribute_key_bytes - .as_bytes() + pub fn max_removed_attribute_key_size(&self) -> usize { + self.values.limits.max_removed_attribute_key_size.as_bytes() } /// The maximum number of seconds a query is allowed to take across retries. diff --git a/relay-dynamic-config/src/project.rs b/relay-dynamic-config/src/project.rs index 79d823e2171..7fa8ed7c350 100644 --- a/relay-dynamic-config/src/project.rs +++ b/relay-dynamic-config/src/project.rs @@ -287,7 +287,7 @@ impl RetentionsConfig { #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct TrimmingConfig { /// The maximum size in bytes above which an item should be trimmed. - pub max_bytes: u32, + pub max_size: u32, } /// Settings for item trimming. diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index d041af6a4d0..cd5e010d212 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -194,14 +194,14 @@ fn normalize_span( let trimming_root_state = { let mut attrs = FieldAttrs::default(); if let Some(span_config) = ctx.project_info.config().trimming.span { - attrs = attrs.max_bytes(span_config.max_bytes as usize); + attrs = attrs.max_bytes(span_config.max_size as usize); } ProcessingState::new_root(Some(Cow::Owned(attrs)), []) }; process_value( span, - &mut eap::TrimmingProcessor::new(ctx.config.max_removed_attribute_key_bytes()), + &mut eap::TrimmingProcessor::new(ctx.config.max_removed_attribute_key_size()), &trimming_root_state, )?; diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 329d0a13f22..1fc70116b6a 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -198,7 +198,7 @@ def test_spansv2_trimming_basic( "retentions": {"span": {"standard": 42, "downsampled": 1337}}, # This is sufficient for all builtin attributes not # to be trimmed. - "trimming": {"span": {"max_bytes": 445}}, + "trimming": {"span": {"max_size": 445}}, } ) From 9ef10975182aa665055ce14ef627a7743f08d577 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 5 Feb 2026 19:30:06 +0100 Subject: [PATCH 13/19] Forgot a rename --- tests/integration/test_spansv2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 1fc70116b6a..3d08ac55570 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -204,7 +204,7 @@ def test_spansv2_trimming_basic( config = { "limits": { - "max_removed_attribute_key_bytes": 30, + "max_removed_attribute_key_size": 30, }, **TEST_CONFIG, } From 19679e16e428c62ca5d3aa824f9df521fcf5112e Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 10:27:56 +0100 Subject: [PATCH 14/19] Fix casing --- relay-dynamic-config/src/project.rs | 1 + tests/integration/test_spansv2.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/relay-dynamic-config/src/project.rs b/relay-dynamic-config/src/project.rs index 7fa8ed7c350..c358d9fdf4c 100644 --- a/relay-dynamic-config/src/project.rs +++ b/relay-dynamic-config/src/project.rs @@ -285,6 +285,7 @@ impl RetentionsConfig { /// Per-category settings for item trimming. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct TrimmingConfig { /// The maximum size in bytes above which an item should be trimmed. pub max_size: u32, diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index 3d08ac55570..ebf9fa5cbf6 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -198,7 +198,7 @@ def test_spansv2_trimming_basic( "retentions": {"span": {"standard": 42, "downsampled": 1337}}, # This is sufficient for all builtin attributes not # to be trimmed. - "trimming": {"span": {"max_size": 445}}, + "trimming": {"span": {"maxSize": 445}}, } ) From 8548aab7f8de7e9d7a82bfe8caace5fe1a8a31ad Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 10:32:39 +0100 Subject: [PATCH 15/19] Remove `bytes_size = 0` annotations --- relay-event-schema/src/protocol/span_v2.rs | 22 +++++++++++----------- tests/integration/test_spansv2.py | 5 +++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/relay-event-schema/src/protocol/span_v2.rs b/relay-event-schema/src/protocol/span_v2.rs index 89e283b02cf..bb77e049a14 100644 --- a/relay-event-schema/src/protocol/span_v2.rs +++ b/relay-event-schema/src/protocol/span_v2.rs @@ -11,35 +11,35 @@ use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId}; #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] pub struct SpanV2 { /// The ID of the trace to which this span belongs. - #[metastructure(required = true, nonempty = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, nonempty = true, trim = false)] pub trace_id: Annotated, /// The ID of the span enclosing this span. - #[metastructure(trim = false, bytes_size = 0)] + #[metastructure(trim = false)] pub parent_span_id: Annotated, /// The Span ID. - #[metastructure(required = true, nonempty = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, nonempty = true, trim = false)] pub span_id: Annotated, /// Span type (see `OperationType` docs). - #[metastructure(required = true, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub name: Annotated, /// The span's status. - #[metastructure(required = true, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub status: Annotated, /// Whether this span is the root span of a segment. - #[metastructure(trim = false, bytes_size = 0)] + #[metastructure(trim = false)] pub is_segment: Annotated, /// Timestamp when the span started. - #[metastructure(required = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub start_timestamp: Annotated, /// Timestamp when the span was ended. - #[metastructure(required = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub end_timestamp: Annotated, /// Links from this span to other spans. @@ -167,15 +167,15 @@ impl IntoValue for SpanV2Status { #[metastructure(trim = false)] pub struct SpanV2Link { /// The trace id of the linked span. - #[metastructure(required = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub trace_id: Annotated, /// The span id of the linked span. - #[metastructure(required = true, trim = false, bytes_size = 0)] + #[metastructure(required = true, trim = false)] pub span_id: Annotated, /// Whether the linked span was positively/negatively sampled. - #[metastructure(trim = false, bytes_size = 0)] + #[metastructure(trim = false)] pub sampled: Annotated, /// Span link attributes, similar to span attributes/data. diff --git a/tests/integration/test_spansv2.py b/tests/integration/test_spansv2.py index ebf9fa5cbf6..b58ef9aaaf4 100644 --- a/tests/integration/test_spansv2.py +++ b/tests/integration/test_spansv2.py @@ -197,8 +197,9 @@ def test_spansv2_trimming_basic( ], "retentions": {"span": {"standard": 42, "downsampled": 1337}}, # This is sufficient for all builtin attributes not - # to be trimmed. - "trimming": {"span": {"maxSize": 445}}, + # to be trimmed. The span fields that aren't trimmed + # also still count for the size limit. + "trimming": {"span": {"maxSize": 453}}, } ) From 7488388c636e73b6d5fa2af145e048f0eed1e8ad Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 10:37:41 +0100 Subject: [PATCH 16/19] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c405174060..867361bdc7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Add new frame fields for MetricKit flamegraphs. ([#5539](https://github.com/getsentry/relay/pull/5539)) - Apply clock drift correction to logs and trace metrics. ([#5609](https://github.com/getsentry/relay/pull/5609)) +- Trim spans with a new EAP trimming processor. ([#5616](https://github.com/getsentry/relay/pull/5616)) **Internal**: From 7f4aa500de97d0ad43e1894df616e79e7ac65e3e Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 11:45:13 +0100 Subject: [PATCH 17/19] Move config setting --- relay-config/src/config.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index 0cb9bdb960d..92bb46310b1 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -657,6 +657,16 @@ pub struct Limits { max_replay_uncompressed_size: ByteSize, /// The maximum size for a replay recording Kafka message. pub max_replay_message_size: ByteSize, + /// The byte size limit up to which Relay will retain + /// keys of invalid/removed attributes. + /// + /// This is only relevant for EAP items (spans, logs, …). + /// In principle, we want to record all deletions of attributes, + /// but we have to institute some limit to protect our infrastructure + /// against excessive metadata sizes. + /// + /// Defaults to 10KiB. + pub max_removed_attribute_key_size: ByteSize, /// The maximum number of threads to spawn for CPU and web work, each. /// /// The total number of threads spawned will roughly be `2 * max_thread_count`. Defaults to @@ -700,16 +710,6 @@ pub struct Limits { /// /// Defaults to `1024`, a value [google has been using for a long time](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=19f92a030ca6d772ab44b22ee6a01378a8cb32d4). pub tcp_listen_backlog: u32, - /// The byte size limit up to which Relay will retain - /// keys of invalid/removed attributes. - /// - /// This is only relevant for EAP items (spans, logs, …). - /// In principle, we want to record all deletions of attributes, - /// but we have to institute some limit to protect our infrastructure - /// against excessive metadata sizes. - /// - /// Defaults to 10KiB. - pub max_removed_attribute_key_size: ByteSize, } impl Default for Limits { From 36b2ba31bcaf13e34d81bd168719c4e5b52a7bd7 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 12:16:42 +0100 Subject: [PATCH 18/19] Add todo --- relay-server/src/processing/spans/process.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index cd5e010d212..1805a8b485b 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -189,6 +189,7 @@ fn normalize_span( eap::write_legacy_attributes(&mut span.attributes); }; + // TODO: Clean this up before merging. // Set a max_bytes value on the root state if it's defined in the project config. // This causes the whole item to be trimmed down to the limit. let trimming_root_state = { From b7f32880d08137519f41aa141f0a03ba93c9645a Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 14:53:09 +0100 Subject: [PATCH 19/19] Use builder --- relay-server/src/processing/spans/process.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index 1805a8b485b..fcd44cf402d 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -1,9 +1,8 @@ -use std::borrow::Cow; use std::collections::BTreeMap; use std::time::Duration; use relay_event_normalization::{GeoIpLookup, RequiredMode, SchemaProcessor, eap}; -use relay_event_schema::processor::{FieldAttrs, ProcessingState, ValueType, process_value}; +use relay_event_schema::processor::{ProcessingState, ValueType, process_value}; use relay_event_schema::protocol::{Span, SpanId, SpanV2}; use relay_protocol::Annotated; @@ -189,21 +188,20 @@ fn normalize_span( eap::write_legacy_attributes(&mut span.attributes); }; - // TODO: Clean this up before merging. // Set a max_bytes value on the root state if it's defined in the project config. // This causes the whole item to be trimmed down to the limit. - let trimming_root_state = { - let mut attrs = FieldAttrs::default(); - if let Some(span_config) = ctx.project_info.config().trimming.span { - attrs = attrs.max_bytes(span_config.max_size as usize); - } - ProcessingState::new_root(Some(Cow::Owned(attrs)), []) - }; + let max_bytes = ctx + .project_info + .config() + .trimming + .span + .map(|cfg| cfg.max_size as usize); + let trimming_root = ProcessingState::root_builder().max_bytes(max_bytes).build(); process_value( span, &mut eap::TrimmingProcessor::new(ctx.config.max_removed_attribute_key_size()), - &trimming_root_state, + &trimming_root, )?; process_value(