relay_event_schema/protocol/
span_v2.rs

1use relay_protocol::{Annotated, Array, Empty, Error, FromValue, IntoValue, Object, Value};
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::Serialize;
7
8use crate::processor::ProcessValue;
9use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId};
10
11/// A version 2 (transactionless) span.
12#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue)]
13pub struct SpanV2 {
14    /// The ID of the trace to which this span belongs.
15    #[metastructure(required = true, trim = false)]
16    pub trace_id: Annotated<TraceId>,
17
18    /// The ID of the span enclosing this span.
19    pub parent_span_id: Annotated<SpanId>,
20
21    /// The Span ID.
22    #[metastructure(required = true, trim = false)]
23    pub span_id: Annotated<SpanId>,
24
25    /// Span type (see `OperationType` docs).
26    #[metastructure(required = true)]
27    pub name: Annotated<OperationType>,
28
29    /// The span's status.
30    #[metastructure(required = true)]
31    pub status: Annotated<SpanV2Status>,
32
33    /// Indicates whether a span's parent is remote.
34    ///
35    /// For OpenTelemetry spans, this is derived from span flags bits 8 and 9. See
36    /// `SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK` and `SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK`.
37    ///
38    /// The states are:
39    ///  - `false`: is not remote
40    ///  - `true`: is remote
41    #[metastructure(required = true)]
42    pub is_remote: Annotated<bool>,
43
44    /// Used to clarify the relationship between parents and children, or to distinguish between
45    /// spans, e.g. a `server` and `client` span with the same name.
46    ///
47    /// See <https://opentelemetry.io/docs/specs/otel/trace/api/#spankind>
48    #[metastructure(skip_serialization = "empty", trim = false)]
49    pub kind: Annotated<SpanV2Kind>,
50
51    /// Timestamp when the span started.
52    #[metastructure(required = true)]
53    pub start_timestamp: Annotated<Timestamp>,
54
55    /// Timestamp when the span was ended.
56    #[metastructure(required = true)]
57    pub end_timestamp: Annotated<Timestamp>,
58
59    /// Links from this span to other spans.
60    #[metastructure(pii = "maybe")]
61    pub links: Annotated<Array<SpanV2Link>>,
62
63    /// Arbitrary attributes on a span.
64    #[metastructure(pii = "true", trim = false)]
65    pub attributes: Annotated<Attributes>,
66
67    /// Additional arbitrary fields for forwards compatibility.
68    #[metastructure(additional_properties, pii = "maybe")]
69    pub other: Object<Value>,
70}
71
72/// Status of a V2 span.
73///
74/// This is a subset of OTEL's statuses (unset, ok, error), plus
75/// a catchall variant for forward compatibility.
76#[derive(Clone, Debug, PartialEq, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum SpanV2Status {
79    /// The span completed successfully.
80    Ok,
81    /// The span contains an error.
82    Error,
83    /// Catchall variant for forward compatibility.
84    Other(String),
85}
86
87impl SpanV2Status {
88    /// Returns the string representation of the status.
89    pub fn as_str(&self) -> &str {
90        match self {
91            Self::Ok => "ok",
92            Self::Error => "error",
93            Self::Other(s) => s,
94        }
95    }
96}
97
98impl Empty for SpanV2Status {
99    #[inline]
100    fn is_empty(&self) -> bool {
101        false
102    }
103}
104
105impl AsRef<str> for SpanV2Status {
106    fn as_ref(&self) -> &str {
107        self.as_str()
108    }
109}
110
111impl fmt::Display for SpanV2Status {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str(self.as_str())
114    }
115}
116
117impl From<String> for SpanV2Status {
118    fn from(value: String) -> Self {
119        match value.as_str() {
120            "ok" => Self::Ok,
121            "error" => Self::Error,
122            _ => Self::Other(value),
123        }
124    }
125}
126
127impl FromValue for SpanV2Status {
128    fn from_value(value: Annotated<Value>) -> Annotated<Self>
129    where
130        Self: Sized,
131    {
132        String::from_value(value).map_value(|s| s.into())
133    }
134}
135
136impl IntoValue for SpanV2Status {
137    fn into_value(self) -> Value
138    where
139        Self: Sized,
140    {
141        Value::String(match self {
142            SpanV2Status::Other(s) => s,
143            _ => self.to_string(),
144        })
145    }
146
147    fn serialize_payload<S>(
148        &self,
149        s: S,
150        _behavior: relay_protocol::SkipSerialization,
151    ) -> Result<S::Ok, S::Error>
152    where
153        Self: Sized,
154        S: serde::Serializer,
155    {
156        s.serialize_str(self.as_str())
157    }
158}
159
160/// The kind of a V2 span.
161///
162/// This corresponds to OTEL's kind enum, plus a
163/// catchall variant for forward compatibility.
164#[derive(Clone, Debug, PartialEq, ProcessValue)]
165pub enum SpanV2Kind {
166    /// An operation internal to an application.
167    Internal,
168    /// Server-side processing requested by a client.
169    Server,
170    /// A request from a client to a server.
171    Client,
172    /// Scheduling of an operation.
173    Producer,
174    /// Processing of a scheduled operation.
175    Consumer,
176}
177
178impl SpanV2Kind {
179    pub fn as_str(&self) -> &'static str {
180        match self {
181            Self::Internal => "internal",
182            Self::Server => "server",
183            Self::Client => "client",
184            Self::Producer => "producer",
185            Self::Consumer => "consumer",
186        }
187    }
188}
189
190impl Empty for SpanV2Kind {
191    fn is_empty(&self) -> bool {
192        false
193    }
194}
195
196impl Default for SpanV2Kind {
197    fn default() -> Self {
198        Self::Internal
199    }
200}
201
202impl fmt::Display for SpanV2Kind {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{}", self.as_str())
205    }
206}
207
208#[derive(Debug, Clone, Copy)]
209pub struct ParseSpanV2KindError;
210
211impl fmt::Display for ParseSpanV2KindError {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(f, "invalid span kind")
214    }
215}
216
217impl FromStr for SpanV2Kind {
218    type Err = ParseSpanV2KindError;
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        let kind = match s {
222            "internal" => Self::Internal,
223            "server" => Self::Server,
224            "client" => Self::Client,
225            "producer" => Self::Producer,
226            "consumer" => Self::Consumer,
227            _ => return Err(ParseSpanV2KindError),
228        };
229        Ok(kind)
230    }
231}
232
233impl FromValue for SpanV2Kind {
234    fn from_value(Annotated(value, meta): Annotated<Value>) -> Annotated<Self>
235    where
236        Self: Sized,
237    {
238        match &value {
239            Some(Value::String(s)) => match s.parse() {
240                Ok(kind) => Annotated(Some(kind), meta),
241                Err(_) => Annotated::from_error(Error::expected("a span kind"), value),
242            },
243            Some(_) => Annotated::from_error(Error::expected("a span kind"), value),
244            None => Annotated::empty(),
245        }
246    }
247}
248
249impl IntoValue for SpanV2Kind {
250    fn into_value(self) -> Value
251    where
252        Self: Sized,
253    {
254        Value::String(self.to_string())
255    }
256
257    fn serialize_payload<S>(
258        &self,
259        s: S,
260        _behavior: relay_protocol::SkipSerialization,
261    ) -> Result<S::Ok, S::Error>
262    where
263        Self: Sized,
264        S: serde::Serializer,
265    {
266        s.serialize_str(self.as_str())
267    }
268}
269
270/// A link from a span to another span.
271#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
272#[metastructure(trim = false)]
273pub struct SpanV2Link {
274    /// The trace id of the linked span.
275    #[metastructure(required = true, trim = false)]
276    pub trace_id: Annotated<TraceId>,
277
278    /// The span id of the linked span.
279    #[metastructure(required = true, trim = false)]
280    pub span_id: Annotated<SpanId>,
281
282    /// Whether the linked span was positively/negatively sampled.
283    #[metastructure(trim = false)]
284    pub sampled: Annotated<bool>,
285
286    /// Span link attributes, similar to span attributes/data.
287    #[metastructure(pii = "maybe", trim = false)]
288    pub attributes: Annotated<Attributes>,
289
290    /// Additional arbitrary fields for forwards compatibility.
291    #[metastructure(additional_properties, pii = "maybe", trim = false)]
292    pub other: Object<Value>,
293}
294
295#[cfg(test)]
296mod tests {
297    use chrono::{TimeZone, Utc};
298    use similar_asserts::assert_eq;
299
300    use super::*;
301
302    #[test]
303    fn test_span_serialization() {
304        let json = r#"{
305  "trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
306  "span_id": "438f40bd3b4a41ee",
307  "name": "GET http://app.test/",
308  "status": "ok",
309  "is_remote": true,
310  "kind": "server",
311  "start_timestamp": 1742921669.25,
312  "end_timestamp": 1742921669.75,
313  "links": [
314    {
315      "trace_id": "627a2885119dcc8184fae7eef09438cb",
316      "span_id": "6c71fc6b09b8b716",
317      "sampled": true,
318      "attributes": {
319        "sentry.link.type": {
320          "type": "string",
321          "value": "previous_trace"
322        }
323      }
324    }
325  ],
326  "attributes": {
327    "custom.error_rate": {
328      "type": "double",
329      "value": 0.5
330    },
331    "custom.is_green": {
332      "type": "boolean",
333      "value": true
334    },
335    "http.response.status_code": {
336      "type": "integer",
337      "value": 200
338    },
339    "sentry.environment": {
340      "type": "string",
341      "value": "local"
342    },
343    "sentry.origin": {
344      "type": "string",
345      "value": "manual"
346    },
347    "sentry.platform": {
348      "type": "string",
349      "value": "php"
350    },
351    "sentry.release": {
352      "type": "string",
353      "value": "1.0.0"
354    },
355    "sentry.sdk.name": {
356      "type": "string",
357      "value": "sentry.php"
358    },
359    "sentry.sdk.version": {
360      "type": "string",
361      "value": "4.10.0"
362    },
363    "sentry.transaction_info.source": {
364      "type": "string",
365      "value": "url"
366    },
367    "server.address": {
368      "type": "string",
369      "value": "DHWKN7KX6N.local"
370    }
371  }
372}"#;
373
374        let mut attributes = Attributes::new();
375
376        attributes.insert("custom.error_rate".to_owned(), 0.5);
377        attributes.insert("custom.is_green".to_owned(), true);
378        attributes.insert("sentry.release".to_owned(), "1.0.0".to_owned());
379        attributes.insert("sentry.environment".to_owned(), "local".to_owned());
380        attributes.insert("sentry.platform".to_owned(), "php".to_owned());
381        attributes.insert("sentry.sdk.name".to_owned(), "sentry.php".to_owned());
382        attributes.insert("sentry.sdk.version".to_owned(), "4.10.0".to_owned());
383        attributes.insert(
384            "sentry.transaction_info.source".to_owned(),
385            "url".to_owned(),
386        );
387        attributes.insert("sentry.origin".to_owned(), "manual".to_owned());
388        attributes.insert("server.address".to_owned(), "DHWKN7KX6N.local".to_owned());
389        attributes.insert("http.response.status_code".to_owned(), 200i64);
390
391        let mut link_attributes = Attributes::new();
392        link_attributes.insert("sentry.link.type".to_owned(), "previous_trace".to_owned());
393
394        let links = vec![Annotated::new(SpanV2Link {
395            trace_id: Annotated::new("627a2885119dcc8184fae7eef09438cb".parse().unwrap()),
396            span_id: Annotated::new("6c71fc6b09b8b716".parse().unwrap()),
397            sampled: Annotated::new(true),
398            attributes: Annotated::new(link_attributes),
399            ..Default::default()
400        })];
401        let span = Annotated::new(SpanV2 {
402            start_timestamp: Annotated::new(
403                Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
404            ),
405            end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
406            name: Annotated::new("GET http://app.test/".to_owned()),
407            trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
408            span_id: Annotated::new("438f40bd3b4a41ee".parse().unwrap()),
409            parent_span_id: Annotated::empty(),
410            status: Annotated::new(SpanV2Status::Ok),
411            kind: Annotated::new(SpanV2Kind::Server),
412            is_remote: Annotated::new(true),
413            links: Annotated::new(links),
414            attributes: Annotated::new(attributes),
415            ..Default::default()
416        });
417        assert_eq!(json, span.to_json_pretty().unwrap());
418
419        let span_from_string = Annotated::from_json(json).unwrap();
420        assert_eq!(span, span_from_string);
421    }
422}