Skip to main content

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