Skip to content

Commit c1898b1

Browse files
feat(processor): Add initial Span Attachment logic (#5363)
Co-authored-by: Joris Bayer <[email protected]>
1 parent d171673 commit c1898b1

File tree

18 files changed

+1490
-55
lines changed

18 files changed

+1490
-55
lines changed

relay-dynamic-config/src/feature.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ pub enum Feature {
129129
#[doc(hidden)]
130130
#[serde(rename = "organizations:indexed-spans-extraction")]
131131
DeprecatedExtractSpansFromEvent,
132+
/// Enable the experimental Span Attachment subset of the Span V2 processing pipeline in Relay.
133+
#[serde(rename = "projects:span-v2-attachment-processing")]
134+
SpanV2AttachmentProcessing,
132135
/// Forward compatibility.
133136
#[doc(hidden)]
134137
#[serde(other)]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
2+
3+
use crate::processor::ProcessValue;
4+
use crate::protocol::{Attributes, Timestamp};
5+
6+
use uuid::Uuid;
7+
8+
/// Metadata for a span attachment.
9+
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
10+
pub struct AttachmentV2Meta {
11+
/// Unique identifier for this attachment.
12+
#[metastructure(required = true, nonempty = true, trim = false)]
13+
pub attachment_id: Annotated<Uuid>,
14+
15+
/// Timestamp when the attachment was created.
16+
#[metastructure(required = true, trim = false)]
17+
pub timestamp: Annotated<Timestamp>,
18+
19+
/// Original filename of the attachment.
20+
#[metastructure(pii = "true", max_chars = 256, max_chars_allowance = 40, trim = false)]
21+
pub filename: Annotated<String>,
22+
23+
/// Content type of the attachment body.
24+
#[metastructure(required = true, max_chars = 128, trim = false)]
25+
pub content_type: Annotated<String>,
26+
27+
/// Arbitrary attributes on a span attachment.
28+
#[metastructure(pii = "maybe")]
29+
pub attributes: Annotated<Attributes>,
30+
31+
/// Additional arbitrary fields for forwards compatibility.
32+
#[metastructure(additional_properties, pii = "maybe")]
33+
pub other: Object<Value>,
34+
}

relay-event-schema/src/protocol/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Implements the sentry event protocol.
22
3+
mod attachment_v2;
34
mod attributes;
45
mod base;
56
mod breadcrumb;
@@ -41,6 +42,7 @@ mod utils;
4142
#[doc(inline)]
4243
pub use relay_base_schema::{events::*, spans::*};
4344

45+
pub use self::attachment_v2::*;
4446
pub use self::attributes::*;
4547
pub use self::breadcrumb::*;
4648
pub use self::breakdowns::*;

relay-server/src/envelope/content_type.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub enum ContentType {
3535
SpanV2Container,
3636
/// `application/vnd.sentry.items.trace-metric+json`
3737
TraceMetricContainer,
38+
/// `application/vnd.sentry.attachment.v2`
39+
AttachmentV2,
3840
/// All integration content types.
3941
Integration(Integration),
4042
/// Any arbitrary content type not listed explicitly.
@@ -57,6 +59,7 @@ impl ContentType {
5759
Self::LogContainer => "application/vnd.sentry.items.log+json",
5860
Self::SpanV2Container => "application/vnd.sentry.items.span.v2+json",
5961
Self::TraceMetricContainer => "application/vnd.sentry.items.trace-metric+json",
62+
Self::AttachmentV2 => "application/vnd.sentry.attachment.v2",
6063
Self::Integration(integration) => integration.as_content_type(),
6164
Self::Other(other) => other,
6265
}
@@ -99,6 +102,8 @@ impl ContentType {
99102
Some(Self::SpanV2Container)
100103
} else if ct.eq_ignore_ascii_case(Self::TraceMetricContainer.as_str()) {
101104
Some(Self::TraceMetricContainer)
105+
} else if ct.eq_ignore_ascii_case(Self::AttachmentV2.as_str()) {
106+
Some(Self::AttachmentV2)
102107
} else {
103108
Integration::from_content_type(ct).map(Self::Integration)
104109
}

relay-server/src/envelope/item.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::ops::AddAssign;
66
use uuid::Uuid;
77

88
use bytes::Bytes;
9-
use relay_event_schema::protocol::EventType;
9+
use relay_event_schema::protocol::{EventType, SpanId};
1010
use relay_protocol::Value;
1111
use relay_quotas::DataCategory;
1212
use serde::{Deserialize, Serialize};
@@ -45,6 +45,8 @@ impl Item {
4545
fully_normalized: false,
4646
profile_type: None,
4747
platform: None,
48+
parent_id: None,
49+
meta_length: None,
4850
},
4951
payload: Bytes::new(),
5052
}
@@ -129,7 +131,7 @@ impl Item {
129131
ItemType::Nel => smallvec![],
130132
ItemType::UnrealReport => smallvec![(DataCategory::Error, item_count)],
131133
ItemType::Attachment => smallvec![
132-
(DataCategory::Attachment, self.len().max(1)),
134+
(DataCategory::Attachment, self.attachment_body_size()),
133135
(DataCategory::AttachmentItem, item_count),
134136
],
135137
ItemType::Session | ItemType::Sessions => {
@@ -434,6 +436,53 @@ impl Item {
434436
self.headers.sampled = sampled;
435437
}
436438

439+
/// Returns the length of the item.
440+
pub fn meta_length(&self) -> Option<u32> {
441+
self.headers.meta_length
442+
}
443+
444+
/// Sets the length of the optional meta segment.
445+
///
446+
/// Only applicable if the item is an attachment.
447+
pub fn set_meta_length(&mut self, meta_length: u32) {
448+
self.headers.meta_length = Some(meta_length);
449+
}
450+
451+
/// Returns the parent entity that this item is associated with, if any.
452+
///
453+
/// Only applicable if the item is an attachment.
454+
pub fn parent_id(&self) -> Option<&ParentId> {
455+
self.headers.parent_id.as_ref()
456+
}
457+
458+
/// Sets the parent entity that this item is associated with.
459+
pub fn set_parent_id(&mut self, parent_id: ParentId) {
460+
self.headers.parent_id = Some(parent_id);
461+
}
462+
463+
/// Returns `true` if this item is an attachment with AttachmentV2 content type.
464+
pub fn is_attachment_v2(&self) -> bool {
465+
self.ty() == &ItemType::Attachment
466+
&& self.content_type() == Some(&ContentType::AttachmentV2)
467+
}
468+
469+
/// Returns the attachment payload size.
470+
///
471+
/// For AttachmentV2, returns only the size of the actual payload, excluding the attachment meta.
472+
/// For Attachment, returns the size of entire payload.
473+
///
474+
/// **Note:** This relies on the `meta_length` header which might not be correct as such this
475+
/// is best effort.
476+
pub fn attachment_body_size(&self) -> usize {
477+
if self.is_attachment_v2() {
478+
self.len()
479+
.saturating_sub(self.meta_length().unwrap_or(0) as usize)
480+
} else {
481+
self.len()
482+
}
483+
.max(1)
484+
}
485+
437486
/// Returns the specified header value, if present.
438487
pub fn get_header<K>(&self, name: &K) -> Option<&Value>
439488
where
@@ -947,6 +996,18 @@ pub struct ItemHeaders {
947996
#[serde(default, skip)]
948997
profile_type: Option<ProfileType>,
949998

999+
/// Content length of an optional meta segment that might be contained in the item.
1000+
///
1001+
/// For the time being such an meta segment is only present for span attachments.
1002+
#[serde(skip_serializing_if = "Option::is_none")]
1003+
meta_length: Option<u32>,
1004+
1005+
/// Parent entity that this item is associated with, if any.
1006+
///
1007+
/// For the time being only applicable if the item is a span-attachment.
1008+
#[serde(flatten, skip_serializing_if = "Option::is_none")]
1009+
parent_id: Option<ParentId>,
1010+
9501011
/// Other attributes for forward compatibility.
9511012
#[serde(flatten)]
9521013
other: BTreeMap<String, Value>,
@@ -994,6 +1055,18 @@ fn is_true(value: &bool) -> bool {
9941055
*value
9951056
}
9961057

1058+
/// Parent identifier for an attachment-v2.
1059+
///
1060+
/// Attachments can be associated with different types of parent entities (only spans for now).
1061+
///
1062+
/// SpanId(None) indicates that the item is a span-attachment that is associated with no specific
1063+
/// span.
1064+
#[derive(Clone, Debug, Deserialize, Serialize)]
1065+
#[serde(rename_all = "snake_case")]
1066+
pub enum ParentId {
1067+
SpanId(Option<SpanId>),
1068+
}
1069+
9971070
#[cfg(test)]
9981071
mod tests {
9991072
use crate::integrations::OtelFormat;

relay-server/src/managed/counted.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ impl Counted for Box<Envelope> {
6161

6262
let data = [
6363
(DataCategory::Attachment, summary.attachment_quantity),
64+
(
65+
DataCategory::AttachmentItem,
66+
summary.attachment_item_quantity,
67+
),
6468
(DataCategory::Profile, summary.profile_quantity),
6569
(DataCategory::ProfileIndexed, summary.profile_quantity),
6670
(DataCategory::Span, summary.span_quantity),

relay-server/src/managed/envelope.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ impl ManagedEnvelope {
362362
);
363363
}
364364

365+
if self.context.summary.attachment_item_quantity > 0 {
366+
self.track_outcome(
367+
outcome.clone(),
368+
DataCategory::AttachmentItem,
369+
self.context.summary.attachment_item_quantity,
370+
);
371+
}
372+
365373
if self.context.summary.monitor_quantity > 0 {
366374
self.track_outcome(
367375
outcome.clone(),

relay-server/src/processing/spans/dynamic_sampling.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub fn validate_dsc(spans: &ExpandedSpans) -> Result<()> {
9191
};
9292

9393
for span in &spans.spans {
94+
let span = &span.span;
9495
let trace_id = get_value!(span.trace_id);
9596

9697
if trace_id != Some(&dsc.trace_id) {
@@ -293,6 +294,7 @@ struct UnsampledSpans {
293294
spans: Vec<Item>,
294295
legacy: Vec<Item>,
295296
integrations: Vec<Item>,
297+
attachments: Vec<Item>,
296298
}
297299

298300
impl From<SerializedSpans> for UnsampledSpans {
@@ -302,12 +304,14 @@ impl From<SerializedSpans> for UnsampledSpans {
302304
spans,
303305
legacy,
304306
integrations,
307+
attachments,
305308
} = value;
306309

307310
Self {
308311
spans,
309312
legacy,
310313
integrations,
314+
attachments,
311315
}
312316
}
313317
}
@@ -318,6 +322,22 @@ impl Counted for UnsampledSpans {
318322
+ outcome_count(&self.legacy)
319323
+ outcome_count(&self.integrations)) as usize;
320324

321-
smallvec::smallvec![(DataCategory::SpanIndexed, quantity)]
325+
let mut quantities = smallvec::smallvec![];
326+
327+
if quantity > 0 {
328+
quantities.push((DataCategory::SpanIndexed, quantity));
329+
}
330+
if !self.attachments.is_empty() {
331+
quantities.push((
332+
DataCategory::Attachment,
333+
self.attachments
334+
.iter()
335+
.map(Item::attachment_body_size)
336+
.sum(),
337+
));
338+
quantities.push((DataCategory::AttachmentItem, self.attachments.len()));
339+
}
340+
341+
quantities
322342
}
323343
}

relay-server/src/processing/spans/filter.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use relay_protocol::Annotated;
55
use crate::extractors::RequestMeta;
66
use crate::managed::Managed;
77
use crate::processing::Context;
8-
use crate::processing::spans::{Error, ExpandedSpans, Result};
8+
use crate::processing::spans::{Error, ExpandedSpans, Result, SerializedSpans};
99

1010
/// Filters standalone spans sent for a project which does not allow standalone span ingestion.
1111
pub fn feature_flag(ctx: Context<'_>) -> Result<()> {
@@ -15,11 +15,25 @@ pub fn feature_flag(ctx: Context<'_>) -> Result<()> {
1515
}
1616
}
1717

18+
// Filters span attachments for a project which does not allow for span attachment ingestion.
19+
pub fn feature_flag_attachment(
20+
spans: Managed<SerializedSpans>,
21+
ctx: Context<'_>,
22+
) -> Managed<SerializedSpans> {
23+
spans.map(|mut spans, r| {
24+
if ctx.should_filter(Feature::SpanV2AttachmentProcessing) {
25+
let attachments = std::mem::take(&mut spans.attachments);
26+
r.reject_err(Error::FilterFeatureFlag, attachments);
27+
}
28+
spans
29+
})
30+
}
31+
1832
/// Applies inbound filters to individual spans.
1933
pub fn filter(spans: &mut Managed<ExpandedSpans>, ctx: Context<'_>) {
2034
spans.retain_with_context(
2135
|spans| (&mut spans.spans, spans.headers.meta()),
22-
|span, meta, _| filter_span(span, meta, ctx),
36+
|span, meta, _| filter_span(&span.span, meta, ctx),
2337
);
2438
}
2539

0 commit comments

Comments
 (0)