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