relay_event_schema/protocol/
span_v2.rs

1use relay_protocol::{Annotated, Array, Empty, FromValue, Getter, IntoValue, Object, Value};
2
3use std::fmt;
4
5use serde::Serialize;
6
7use crate::processor::ProcessValue;
8use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId};
9
10/// A version 2 (transactionless) span.
11#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
12pub struct SpanV2 {
13    /// The ID of the trace to which this span belongs.
14    #[metastructure(required = true, nonempty = true, trim = false)]
15    pub trace_id: Annotated<TraceId>,
16
17    /// The ID of the span enclosing this span.
18    #[metastructure(trim = false)]
19    pub parent_span_id: Annotated<SpanId>,
20
21    /// The Span ID.
22    #[metastructure(required = true, nonempty = true, trim = false)]
23    pub span_id: Annotated<SpanId>,
24
25    /// Span type (see `OperationType` docs).
26    #[metastructure(required = true, trim = false)]
27    pub name: Annotated<OperationType>,
28
29    /// The span's status.
30    #[metastructure(required = true, trim = false)]
31    pub status: Annotated<SpanV2Status>,
32
33    /// Whether this span is the root span of a segment.
34    #[metastructure(trim = false)]
35    pub is_segment: Annotated<bool>,
36
37    /// Timestamp when the span started.
38    #[metastructure(required = true, trim = false)]
39    pub start_timestamp: Annotated<Timestamp>,
40
41    /// Timestamp when the span was ended.
42    #[metastructure(required = true, trim = false)]
43    pub end_timestamp: Annotated<Timestamp>,
44
45    /// Links from this span to other spans.
46    #[metastructure(pii = "maybe", trim = true)]
47    pub links: Annotated<Array<SpanV2Link>>,
48
49    /// Arbitrary attributes on a span.
50    #[metastructure(pii = "true", trim = true)]
51    pub attributes: Annotated<Attributes>,
52
53    /// Additional arbitrary fields for forwards compatibility.
54    #[metastructure(additional_properties, pii = "maybe")]
55    pub other: Object<Value>,
56}
57
58impl Getter for SpanV2 {
59    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
60        Some(match path.strip_prefix("span.")? {
61            "name" => self.name.value()?.as_str().into(),
62            "status" => self.status.value()?.as_str().into(),
63            path => {
64                if let Some(key) = path.strip_prefix("attributes.") {
65                    let key = key.strip_suffix(".value")?;
66                    self.attributes.value()?.get_value(key)?.into()
67                } else {
68                    return None;
69                }
70            }
71        })
72    }
73}
74
75/// Status of a V2 span.
76///
77/// This is a subset of OTEL's statuses (unset, ok, error), plus
78/// a catchall variant for forward compatibility.
79#[derive(Clone, Debug, PartialEq, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum SpanV2Status {
82    /// The span completed successfully.
83    Ok,
84    /// The span contains an error.
85    Error,
86    /// Catchall variant for forward compatibility.
87    Other(String),
88}
89
90impl SpanV2Status {
91    /// Returns the string representation of the status.
92    pub fn as_str(&self) -> &str {
93        match self {
94            Self::Ok => "ok",
95            Self::Error => "error",
96            Self::Other(s) => s,
97        }
98    }
99}
100
101impl Empty for SpanV2Status {
102    #[inline]
103    fn is_empty(&self) -> bool {
104        false
105    }
106}
107
108impl ProcessValue for SpanV2Status {}
109
110impl AsRef<str> for SpanV2Status {
111    fn as_ref(&self) -> &str {
112        self.as_str()
113    }
114}
115
116impl fmt::Display for SpanV2Status {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(self.as_str())
119    }
120}
121
122impl From<String> for SpanV2Status {
123    fn from(value: String) -> Self {
124        match value.as_str() {
125            "ok" => Self::Ok,
126            "error" => Self::Error,
127            _ => Self::Other(value),
128        }
129    }
130}
131
132impl FromValue for SpanV2Status {
133    fn from_value(value: Annotated<Value>) -> Annotated<Self>
134    where
135        Self: Sized,
136    {
137        String::from_value(value).map_value(|s| s.into())
138    }
139}
140
141impl IntoValue for SpanV2Status {
142    fn into_value(self) -> Value
143    where
144        Self: Sized,
145    {
146        Value::String(match self {
147            SpanV2Status::Other(s) => s,
148            _ => self.to_string(),
149        })
150    }
151
152    fn serialize_payload<S>(
153        &self,
154        s: S,
155        _behavior: relay_protocol::SkipSerialization,
156    ) -> Result<S::Ok, S::Error>
157    where
158        Self: Sized,
159        S: serde::Serializer,
160    {
161        s.serialize_str(self.as_str())
162    }
163}
164
165/// A link from a span to another span.
166#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
167#[metastructure(trim = false)]
168pub struct SpanV2Link {
169    /// The trace id of the linked span.
170    #[metastructure(required = true, trim = false)]
171    pub trace_id: Annotated<TraceId>,
172
173    /// The span id of the linked span.
174    #[metastructure(required = true, trim = false)]
175    pub span_id: Annotated<SpanId>,
176
177    /// Whether the linked span was positively/negatively sampled.
178    #[metastructure(trim = false)]
179    pub sampled: Annotated<bool>,
180
181    /// Span link attributes, similar to span attributes/data.
182    #[metastructure(pii = "maybe", trim = true)]
183    pub attributes: Annotated<Attributes>,
184
185    /// Additional arbitrary fields for forwards compatibility.
186    #[metastructure(additional_properties, pii = "maybe")]
187    pub other: Object<Value>,
188}
189
190#[cfg(test)]
191mod tests {
192    use chrono::{TimeZone, Utc};
193    use similar_asserts::assert_eq;
194
195    use super::*;
196
197    #[test]
198    fn test_span_serialization() {
199        let json = r#"{
200  "trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
201  "span_id": "438f40bd3b4a41ee",
202  "name": "GET http://app.test/",
203  "status": "ok",
204  "is_segment": true,
205  "start_timestamp": 1742921669.25,
206  "end_timestamp": 1742921669.75,
207  "links": [
208    {
209      "trace_id": "627a2885119dcc8184fae7eef09438cb",
210      "span_id": "6c71fc6b09b8b716",
211      "sampled": true,
212      "attributes": {
213        "sentry.link.type": {
214          "type": "string",
215          "value": "previous_trace"
216        }
217      }
218    }
219  ],
220  "attributes": {
221    "custom.error_rate": {
222      "type": "double",
223      "value": 0.5
224    },
225    "custom.is_green": {
226      "type": "boolean",
227      "value": true
228    },
229    "http.response.status_code": {
230      "type": "integer",
231      "value": 200
232    },
233    "sentry.environment": {
234      "type": "string",
235      "value": "local"
236    },
237    "sentry.origin": {
238      "type": "string",
239      "value": "manual"
240    },
241    "sentry.platform": {
242      "type": "string",
243      "value": "php"
244    },
245    "sentry.release": {
246      "type": "string",
247      "value": "1.0.0"
248    },
249    "sentry.sdk.name": {
250      "type": "string",
251      "value": "sentry.php"
252    },
253    "sentry.sdk.version": {
254      "type": "string",
255      "value": "4.10.0"
256    },
257    "sentry.transaction_info.source": {
258      "type": "string",
259      "value": "url"
260    },
261    "server.address": {
262      "type": "string",
263      "value": "DHWKN7KX6N.local"
264    }
265  }
266}"#;
267
268        let mut attributes = Attributes::new();
269
270        attributes.insert("custom.error_rate".to_owned(), 0.5);
271        attributes.insert("custom.is_green".to_owned(), true);
272        attributes.insert("sentry.release".to_owned(), "1.0.0".to_owned());
273        attributes.insert("sentry.environment".to_owned(), "local".to_owned());
274        attributes.insert("sentry.platform".to_owned(), "php".to_owned());
275        attributes.insert("sentry.sdk.name".to_owned(), "sentry.php".to_owned());
276        attributes.insert("sentry.sdk.version".to_owned(), "4.10.0".to_owned());
277        attributes.insert(
278            "sentry.transaction_info.source".to_owned(),
279            "url".to_owned(),
280        );
281        attributes.insert("sentry.origin".to_owned(), "manual".to_owned());
282        attributes.insert("server.address".to_owned(), "DHWKN7KX6N.local".to_owned());
283        attributes.insert("http.response.status_code".to_owned(), 200i64);
284
285        let mut link_attributes = Attributes::new();
286        link_attributes.insert("sentry.link.type".to_owned(), "previous_trace".to_owned());
287
288        let links = vec![Annotated::new(SpanV2Link {
289            trace_id: Annotated::new("627a2885119dcc8184fae7eef09438cb".parse().unwrap()),
290            span_id: Annotated::new("6c71fc6b09b8b716".parse().unwrap()),
291            sampled: Annotated::new(true),
292            attributes: Annotated::new(link_attributes),
293            ..Default::default()
294        })];
295        let span = Annotated::new(SpanV2 {
296            start_timestamp: Annotated::new(
297                Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
298            ),
299            end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
300            name: Annotated::new("GET http://app.test/".to_owned()),
301            trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
302            span_id: Annotated::new("438f40bd3b4a41ee".parse().unwrap()),
303            parent_span_id: Annotated::empty(),
304            status: Annotated::new(SpanV2Status::Ok),
305            is_segment: Annotated::new(true),
306            links: Annotated::new(links),
307            attributes: Annotated::new(attributes),
308            ..Default::default()
309        });
310        assert_eq!(json, span.to_json_pretty().unwrap());
311
312        let span_from_string = Annotated::from_json(json).unwrap();
313        assert_eq!(span, span_from_string);
314    }
315}