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