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