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#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue)]
15pub struct SpanV2 {
16 #[metastructure(required = true, trim = false)]
18 pub trace_id: Annotated<TraceId>,
19
20 pub parent_span_id: Annotated<SpanId>,
22
23 #[metastructure(required = true, trim = false)]
25 pub span_id: Annotated<SpanId>,
26
27 #[metastructure(required = true)]
29 pub name: Annotated<OperationType>,
30
31 #[metastructure(required = true)]
33 pub status: Annotated<SpanV2Status>,
34
35 #[metastructure(required = true)]
44 pub is_remote: Annotated<bool>,
45
46 #[metastructure(skip_serialization = "empty", trim = false)]
51 pub kind: Annotated<SpanV2Kind>,
52
53 #[metastructure(required = true)]
55 pub start_timestamp: Annotated<Timestamp>,
56
57 #[metastructure(required = true)]
59 pub end_timestamp: Annotated<Timestamp>,
60
61 #[metastructure(pii = "maybe")]
63 pub links: Annotated<Array<SpanV2Link>>,
64
65 #[metastructure(pii = "true", trim = false)]
67 pub attributes: Annotated<Object<Attribute>>,
68
69 #[metastructure(additional_properties, pii = "maybe")]
71 pub other: Object<Value>,
72}
73
74impl SpanV2 {
75 pub fn attribute(&self, key: &str) -> Option<&Annotated<Value>> {
77 Some(&self.attributes.value()?.get(key)?.value()?.value.value)
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Serialize)]
86#[serde(rename_all = "snake_case")]
87pub enum SpanV2Status {
88 Ok,
90 Error,
92 Other(String),
94}
95
96impl SpanV2Status {
97 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#[derive(Clone, Debug, PartialEq, ProcessValue)]
174pub enum SpanV2Kind {
175 Internal,
177 Server,
179 Client,
181 Producer,
183 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#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
281#[metastructure(trim = false)]
282pub struct SpanV2Link {
283 #[metastructure(required = true, trim = false)]
285 pub trace_id: Annotated<TraceId>,
286
287 #[metastructure(required = true, trim = false)]
289 pub span_id: Annotated<SpanId>,
290
291 #[metastructure(trim = false)]
293 pub sampled: Annotated<bool>,
294
295 #[metastructure(pii = "maybe", trim = false)]
297 pub attributes: Annotated<Object<Attribute>>,
298
299 #[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}